├── Sukhasana
├── Images.xcassets
│ ├── Gear.imageset
│ │ ├── Gear@2x.png
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon-16.png
│ │ ├── icon-32.png
│ │ ├── icon-128.png
│ │ ├── icon-16@2x.png
│ │ ├── icon-256.png
│ │ ├── icon-32@2x.png
│ │ ├── icon-512.png
│ │ ├── icon-128@2x.png
│ │ ├── icon-256@2x.png
│ │ ├── icon-512@2x.png
│ │ └── Contents.json
│ ├── StatusItemIcon.imageset
│ │ ├── StatusItemIcon.png
│ │ ├── StatusItemIcon@2x.png
│ │ └── Contents.json
│ └── StatusItemIconHighlighted.imageset
│ │ ├── menu-white.png
│ │ ├── menu-white@2x.png
│ │ └── Contents.json
├── Model
│ ├── Settings.swift
│ └── Results.swift
├── View
│ ├── ViewController.swift
│ ├── SeparatorView.swift
│ ├── Panel.swift
│ ├── MainViewController.swift
│ ├── SettingsViewController.swift
│ ├── AppDelegate.swift
│ ├── ResultsTableView.swift
│ ├── SettingsViewController.xib
│ ├── MainViewController.xib
│ └── MainMenu.xib
├── Other
│ ├── NSUserDefaults+SettingsStore.swift
│ ├── SignalOperators.swift
│ └── APIClient.swift
├── Info.plist
└── ViewModel
│ ├── ApplicationModel.swift
│ ├── ResultsTableViewModel.swift
│ ├── MainScreenModel.swift
│ └── SettingsScreenModel.swift
├── Cartfile
├── Cartfile.resolved
├── Makefile
├── Sukhasana.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ └── Sukhasana.xcscheme
└── project.pbxproj
├── .gitignore
├── SukhasanaTests
├── Info.plist
└── SukhasanaTests.swift
├── LICENSE.md
└── README.md
/Sukhasana/Images.xcassets/Gear.imageset/Gear@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/Gear.imageset/Gear@2x.png
--------------------------------------------------------------------------------
/Cartfile:
--------------------------------------------------------------------------------
1 | github "brow/Alamofire" == 1.1.4
2 | github "brow/MASShortcut" "carthage"
3 | github "brow/ReactiveCocoa" "7651d7de1c939b2a4db7ffb3220ca6274c20a9aa"
4 |
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-16@2x.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-32@2x.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-128@2x.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-256@2x.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/AppIcon.appiconset/icon-512@2x.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/StatusItemIcon.imageset/StatusItemIcon@2x.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white.png
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brow/Sukhasana/HEAD/Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/menu-white@2x.png
--------------------------------------------------------------------------------
/Cartfile.resolved:
--------------------------------------------------------------------------------
1 | github "brow/Alamofire" "1.1.4"
2 | github "LlamaKit/LlamaKit" "v0.5.0"
3 | github "brow/MASShortcut" "4d3c2cad2c35e466fb28c9718e2d926b951b3066"
4 | github "brow/ReactiveCocoa" "7651d7de1c939b2a4db7ffb3220ca6274c20a9aa"
5 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | APP=build/Release/Sukhasana.app
2 |
3 | $(APP):
4 | carthage bootstrap --platform Mac
5 | xcodebuild -configuration Release
6 |
7 | install: $(APP)
8 | cp -r $(APP) /Applications
9 |
10 | clean:
11 | rm -rf build
12 |
--------------------------------------------------------------------------------
/Sukhasana.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sukhasana/Model/Settings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Settings.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/21/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | struct Settings {
10 | let APIKey, workspaceID: String
11 | }
--------------------------------------------------------------------------------
/Sukhasana/View/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/21/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | protocol ViewController {
12 | var view: NSView { get }
13 | func viewDidDisplay()
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | build/
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | xcuserdata
13 | *.xccheckout
14 | *.moved-aside
15 | DerivedData
16 | *.hmap
17 | *.ipa
18 | *.xcuserstate
19 |
20 | # Carthage
21 | #
22 | Carthage
23 |
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/Gear.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "Gear@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Sukhasana/View/SeparatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SeparatorView.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/22/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class SeparatorView: NSView {
12 | override func drawRect(dirtyRect: NSRect) {
13 | super.drawRect(dirtyRect)
14 | NSColor(calibratedWhite: 0.8, alpha: 1).setFill()
15 | NSRectFill(dirtyRect)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/StatusItemIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "StatusItemIcon.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "StatusItemIcon@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/StatusItemIconHighlighted.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "menu-white.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "menu-white@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Sukhasana/Model/Results.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Results.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 3/22/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Results {
12 | let projects, tasks: [Result]
13 |
14 | static var empty: Results {
15 | return Results(projects: [], tasks: [])
16 | }
17 | }
18 |
19 | struct Result {
20 | let name, id: String
21 | var URL: NSURL {
22 | // FIXME: escape id
23 | return NSURL(string: "https://app.asana.com/0/\(id)/\(id)")!
24 | }
25 | }
--------------------------------------------------------------------------------
/Sukhasana/View/Panel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Panel.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/7/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class Panel: NSPanel {
12 | // MARK: NSWindow
13 |
14 | override var canBecomeKeyWindow: Bool {
15 | return true
16 | }
17 |
18 | override func recalculateKeyViewLoop() {
19 | // If this is not overridden, then the nextKeyView outlets set in the nibs
20 | // are ignored the first time that a given view is displayed in the panel.
21 | // This is not helped by unsetting NSPanel.autorecalculatesKeyViewLoop.
22 | }
23 |
24 | // MARK: NSResponder
25 |
26 | func cancel(sender: AnyObject?) {
27 | // Called during Escape or Command-. shortcuts.
28 | close()
29 | }
30 | }
--------------------------------------------------------------------------------
/Sukhasana/Other/NSUserDefaults+SettingsStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSUserDefaults+SettingsStore.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/21/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | private let APIKeyKey = "APIKey", workspaceIDKey = "workspaceID"
12 |
13 | extension NSUserDefaults: SettingsStore {
14 |
15 | func saveSettings(settings: Settings) -> () {
16 | setObject(settings.APIKey, forKey: APIKeyKey)
17 | setObject(settings.workspaceID, forKey: workspaceIDKey)
18 | }
19 |
20 | func restoreSettings() -> Settings? {
21 | if let APIKey = stringForKey(APIKeyKey) {
22 | if let workspaceID = stringForKey(workspaceIDKey) {
23 | return Settings(APIKey: APIKey, workspaceID: workspaceID)
24 | }
25 | }
26 | return nil
27 | }
28 | }
--------------------------------------------------------------------------------
/SukhasanaTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | com.tombrow.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/SukhasanaTests/SukhasanaTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SukhasanaTests.swift
3 | // SukhasanaTests
4 | //
5 | // Created by Tom Brow on 2/4/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import XCTest
11 |
12 | class SukhasanaTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | XCTAssert(true, "Pass")
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measureBlock() {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | **Copyright (c) 2015, Tom Brow**
2 | **All rights reserved.**
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | this software and associated documentation files (the "Software"), to deal in
6 | the Software without restriction, including without limitation the rights to
7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | the Software, and to permit persons to whom the Software is furnished to do so,
9 | subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sukhasana
2 | Mac status item for searching Asana. An experiment with [MVVM](https://github.com/ReactiveCocoa/ReactiveViewModel) in [ReactiveCocoa 3](https://github.com/ReactiveCocoa/ReactiveCocoa/pull/1382).
3 |
4 | 
5 |
6 | Requires OS X Yosemite (10.10).
7 |
8 | ## Installation
9 |
10 | ### Binary
11 |
12 | 1. Download `Sukhasana.zip` from the [latest release](https://github.com/brow/Sukhasana/releases/latest).
13 | 2. Open `Sukhasana.zip`.
14 | 3. Move `Sukhasana` into your `Applications` folder.
15 |
16 | ### From source
17 | Sukhasana's dependencies are built with [Carthage](https://github.com/Carthage/Carthage). You can install Carthage using [Homebrew](http://brew.sh/).
18 | ```
19 | brew install carthage
20 | ```
21 | Build and install with `make`.
22 | ```
23 | git clone git@github.com:brow/Sukhasana.git
24 | cd Sukhasana
25 | make install
26 | ```
27 | Be sure to `make clean` when you update.
28 | ```
29 | git pull
30 | make clean install
31 | ```
32 |
33 | ## Development
34 | ```
35 | brew install carthage
36 | git clone git@github.com:brow/Sukhasana.git
37 | cd Sukhasana
38 | carthage bootstrap --platform Mac
39 | open Sukhasana.xcodeproj
40 | ```
41 |
--------------------------------------------------------------------------------
/Sukhasana/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | com.tombrow.$(PRODUCT_NAME:rfc1034identifier)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.1.1
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 3
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | LSUIElement
28 |
29 | NSHumanReadableCopyright
30 | Copyright © 2015 Tom Brow. All rights reserved.
31 | NSMainNibFile
32 | MainMenu
33 | NSPrincipalClass
34 | NSApplication
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Sukhasana/Other/SignalOperators.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignalOperators.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/16/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import ReactiveCocoa
10 |
11 | func startWith(value: T)(producer: ReactiveCocoa.SignalProducer) -> ReactiveCocoa.SignalProducer {
12 | return SignalProducer(value: value) |> concat(producer)
13 | }
14 |
15 | func catchTo(value: T)(producer: ReactiveCocoa.SignalProducer) -> ReactiveCocoa.SignalProducer {
16 | return catch({ _ in SignalProducer(value: value) })(producer: producer)
17 | }
18 |
19 | func propertyOf(initialValue: T, producer: SignalProducer) -> PropertyOf {
20 | let mutableProperty = MutableProperty(initialValue)
21 | mutableProperty <~ producer
22 | return PropertyOf(mutableProperty)
23 | }
24 |
25 | func replay(capacity: Int = Int.max)(producer: ReactiveCocoa.SignalProducer) -> ReactiveCocoa.SignalProducer {
26 | let (returnedProducer, sink) = SignalProducer.buffer(capacity)
27 | producer.start(sink)
28 | return returnedProducer
29 | }
30 |
31 | func mapOptional(transform: T -> U?)(signal: Signal) -> Signal {
32 | return signal |> map(transform) |> filter { $0 != nil } |> map { $0! }
33 | }
--------------------------------------------------------------------------------
/Sukhasana/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon-16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon-16@2x.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "icon-32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "icon-32@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "icon-128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "icon-128@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "icon-256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "icon-256@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "icon-512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "icon-512@2x.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Sukhasana/Other/APIClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Network.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/16/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import ReactiveCocoa
10 | import Alamofire
11 |
12 | struct APIClient {
13 | init(APIKey: String) {
14 | let config = NSURLSessionConfiguration.defaultSessionConfiguration()
15 | config.HTTPAdditionalHeaders = ["Authorization": "Basic " + ("\(APIKey):" as NSString)
16 | .dataUsingEncoding(NSUTF8StringEncoding)!
17 | .base64EncodedStringWithOptions(NSDataBase64EncodingOptions.allZeros)]
18 |
19 | requestManager = Manager(configuration: config)
20 | }
21 |
22 | enum TypeaheadType: String {
23 | case Task = "task"
24 | case Project = "project"
25 | }
26 |
27 | func requestTypeaheadResultsInWorkspace(workspaceID: String, ofType type: TypeaheadType, matchingQuery query: String) -> SignalProducer {
28 | let typeaheadType = "task"
29 |
30 | return SignalProducer { observer, _ in
31 | self.requestManager
32 | .request(
33 | .GET,
34 | // FIXME: escape workspaceID
35 | "https://app.asana.com/api/1.0/workspaces/\(workspaceID)/typeahead",
36 | parameters: ["type": type.rawValue, "query": query, "count": 10])
37 | .validate()
38 | .responseJSON {_, _, JSON, error in
39 | if let error = error {
40 | sendError(observer, error)
41 | } else {
42 | if let dict = JSON as? NSDictionary {
43 | sendNext(observer, dict)
44 | }
45 | sendCompleted(observer)
46 | }
47 | }
48 | return
49 | }
50 | }
51 |
52 | func requestWorkspaces() -> SignalProducer {
53 | return SignalProducer { observer, _ in
54 | self.requestManager
55 | .request(
56 | .GET,
57 | "https://app.asana.com/api/1.0/workspaces")
58 | .validate()
59 | .responseJSON {_, _, JSON, error in
60 | if let error = error {
61 | sendError(observer, error)
62 | } else {
63 | if let dict = JSON as? NSDictionary {
64 | sendNext(observer, dict)
65 | }
66 | sendCompleted(observer)
67 | }
68 | }
69 | return
70 | }
71 | }
72 |
73 | // MARK: private
74 | private let requestManager: Alamofire.Manager
75 | }
76 |
--------------------------------------------------------------------------------
/Sukhasana/View/MainViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewController.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/12/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import ReactiveCocoa
11 |
12 | class MainViewController: NSViewController, ViewController, NSTextFieldDelegate {
13 | @IBOutlet var textField: NSTextField!
14 | @IBOutlet var resultsTableView: ResultsTableView!
15 | @IBOutlet var resultsTableViewHeightConstraint: NSLayoutConstraint!
16 | @IBOutlet var progressIndicator: NSProgressIndicator!
17 |
18 | init?(model: MainScreenModel, delegate: MainViewControllerDelegate) {
19 | self.model = model
20 | self.delegate = delegate
21 |
22 | super.init(nibName: "MainViewController", bundle: nil)
23 | }
24 |
25 | required init?(coder: NSCoder) {
26 | fatalError("not supported")
27 | }
28 |
29 | @IBAction func didClickSettingsButton(sender: AnyObject?) {
30 | sendNext(model.didClickSettingsButton, ())
31 | }
32 |
33 | // MARK: ViewController
34 |
35 | func viewDidDisplay() {
36 | view.window?.makeFirstResponder(textField)
37 | }
38 |
39 | // MARK: NSTextFieldDelegate
40 |
41 | override func controlTextDidChange(obj: NSNotification) {
42 | model.textFieldText.put(textField!.stringValue)
43 | }
44 |
45 | func control(control: NSControl, textView: NSTextView, doCommandBySelector commandSelector: Selector) -> Bool {
46 | switch commandSelector {
47 | case "moveDown:":
48 | // Down arrow key moves from search field to results table
49 | control.window?.selectKeyViewFollowingView(control)
50 | return true
51 | default:
52 | return false
53 | }
54 | }
55 |
56 | // MARK: NSViewController
57 |
58 | override func loadView() {
59 | super.loadView()
60 |
61 | resultsTableView.model <~ model.resultsTableViewModel
62 |
63 | resultsTableView.fittingHeight.start { [weak self] height in
64 | if let _self = self {
65 | _self.resultsTableViewHeightConstraint.constant = height
66 | _self.delegate?.mainViewControllerDidChangeFittingSize(_self)
67 | }
68 | }
69 |
70 | model.activityIndicatorIsAnimating.producer.start { [weak self] animating in
71 | if animating {
72 | self?.progressIndicator.startAnimation(self)
73 | } else {
74 | self?.progressIndicator.stopAnimation(self)
75 | }
76 | }
77 | }
78 |
79 | // MARK: private
80 |
81 | private let model: MainScreenModel
82 | private weak var delegate: MainViewControllerDelegate?
83 | }
84 |
85 | protocol MainViewControllerDelegate: NSObjectProtocol {
86 | func mainViewControllerDidChangeFittingSize(mainViewController: MainViewController)
87 | }
--------------------------------------------------------------------------------
/Sukhasana/View/SettingsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsViewController.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/16/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import ReactiveCocoa
11 | import MASShortcut
12 |
13 | class SettingsViewController: NSViewController, ViewController, NSTextFieldDelegate {
14 | @IBOutlet var APIKeyTextField: NSTextField!
15 | @IBOutlet var workspacePopUpButton: NSPopUpButton!
16 | @IBOutlet var saveButton: NSButton!
17 | @IBOutlet var progressIndicator: NSProgressIndicator!
18 | @IBOutlet var shortcutView: MASShortcutView!
19 |
20 | init?(model: SettingsScreenModel) {
21 | self.model = model
22 |
23 | super.init(nibName: "SettingsViewController", bundle: nil)
24 | }
25 |
26 | required init?(coder: NSCoder) {
27 | fatalError("not supported")
28 | }
29 |
30 | @IBAction func didClickSaveButton(sender: AnyObject?) {
31 | sendNext(model.didClickSaveButton, ())
32 | }
33 |
34 | @IBAction func workspacePopUpButtonDidSelectItem(sender: AnyObject?) {
35 | sendNext(model.workspacePopUpDidSelectItemAtIndex, workspacePopUpButton.indexOfSelectedItem)
36 | }
37 |
38 | // MARK: ViewController
39 |
40 | func viewDidDisplay() {
41 | view.window?.makeFirstResponder(APIKeyTextField)
42 | }
43 |
44 | // MARK: NSTextFieldDelegate
45 |
46 | override func controlTextDidChange(obj: NSNotification) {
47 | model.APIKeyTextFieldText.put(APIKeyTextField.stringValue)
48 | }
49 |
50 | // MARK: NSViewController
51 |
52 | override func loadView() {
53 | super.loadView()
54 |
55 | model.saveButtonEnabled.producer.start { [weak self] enabled in
56 | self?.saveButton?.enabled = enabled
57 | return
58 | }
59 | model.progressIndicatorAnimating.producer.start { [weak self] animating in
60 | if animating {
61 | self?.progressIndicator.startAnimation(nil)
62 | } else {
63 | self?.progressIndicator.stopAnimation(nil)
64 | }
65 | }
66 | model.workspacePopUpButtonEnabled.producer.start { [weak self] enabled in
67 | self?.workspacePopUpButton?.enabled = enabled
68 | return
69 | }
70 | model.workspacePopUpItemsTitles.producer.start { [weak self] titles in
71 | self?.workspacePopUpButton.removeAllItems()
72 | self?.workspacePopUpButton.addItemsWithTitles(titles)
73 | }
74 |
75 | APIKeyTextField.stringValue = model.APIKeyTextFieldText.value
76 | workspacePopUpButton.selectItemAtIndex(model.workspacePopupSelectedIndex.value)
77 | shortcutView.associatedUserDefaultsKey = model.shortcutViewAssociatedUserDefaultsKey
78 | }
79 |
80 | // MARK: private
81 |
82 | private let model: SettingsScreenModel
83 | }
84 |
--------------------------------------------------------------------------------
/Sukhasana/ViewModel/ApplicationModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationModel.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/16/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import ReactiveCocoa
10 |
11 | struct ApplicationModel {
12 | enum Screen {
13 | case Settings(SettingsScreenModel)
14 | case Main(MainScreenModel)
15 | }
16 |
17 | enum Effect {
18 | case DisplayScreen(Screen)
19 | case Results(ResultsTableViewModel.Effect)
20 | }
21 |
22 | let effects: SignalProducer
23 | let shouldOpenPanelOnLaunch: Bool
24 |
25 | init(settingsStore: SettingsStore, globalShortcutDefaultsKey: String) {
26 | let restoredSettings = settingsStore.restoreSettings()
27 | let (settingsModel, didSaveSettings) = SettingsScreenModel.makeWithSettings(
28 | restoredSettings,
29 | globalShortcutDefaultsKey: globalShortcutDefaultsKey)
30 |
31 | // Persist saved settings
32 | didSaveSettings.start { settings in
33 | settingsStore.saveSettings(settings)
34 | }
35 |
36 | // Show the main screen right away if settings already exist
37 | let didRestoreSettings: SignalProducer = {
38 | if let restoredSettings = restoredSettings {
39 | return SignalProducer(value: restoredSettings)
40 | } else {
41 | return SignalProducer.empty
42 | }
43 | }()
44 |
45 | // Show the main screen after new settings are saved
46 | let mainModelsAndEffects = didRestoreSettings
47 | |> concat(didSaveSettings)
48 | |> map(MainScreenModel.makeWithSettings)
49 | |> replay(capacity: 1)
50 | let shouldDisplayMainScreen = mainModelsAndEffects
51 | |> map { Screen.Main($0.0) }
52 |
53 | // Show the settings screen after the Settings button is clicked, and on
54 | // launch if no settings have been restored
55 | let shouldDisplaySettingsScreen = SignalProducer(values: restoredSettings == nil ? [()] : [])
56 | |> concat (
57 | mainModelsAndEffects
58 | |> joinMap(.Latest) { $0.effects }
59 | |> filter {
60 | switch $0 {
61 | case .OpenSettings:
62 | return true
63 | default:
64 | return false
65 | }
66 | }
67 | |> map { _ in () }
68 | )
69 | |> map { _ in Screen.Settings(settingsModel) }
70 |
71 | // If settings haven't been entered yet, this is probably the first launch.
72 | // We should open the panel to show the user where it is.
73 | shouldOpenPanelOnLaunch = restoredSettings == nil
74 |
75 | effects = SignalProducer(values: [
76 | shouldDisplayMainScreen
77 | |> map { .DisplayScreen($0) },
78 | shouldDisplaySettingsScreen
79 | |> map { .DisplayScreen($0) },
80 | mainModelsAndEffects
81 | |> joinMap(.Latest) { $0.effects}
82 | |> mapOptional { effect in
83 | switch effect {
84 | case .Results(let effect):
85 | return .Results(effect)
86 | default:
87 | return nil
88 | }
89 | },
90 | ])
91 | |> join(.Merge)
92 | }
93 | }
94 |
95 | protocol SettingsStore {
96 | func saveSettings(settings: Settings) -> ()
97 | func restoreSettings() -> Settings?
98 | }
99 |
--------------------------------------------------------------------------------
/Sukhasana/ViewModel/ResultsTableViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultsScreenModel.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 3/22/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import ReactiveCocoa
11 |
12 | struct ResultsTableViewModel {
13 | enum Cell {
14 | case Selectable(String)
15 | case Separator
16 | }
17 |
18 | enum Action {
19 | case Click
20 | case Copy
21 | }
22 |
23 | enum Effect {
24 | case OpenURL(NSURL)
25 | case WriteObjectsToPasteboard([NSPasteboardWriting])
26 | }
27 |
28 | let didRecognizeActionOnRowAtIndex: Signal<(Action, Int), NoError>.Observer
29 |
30 | static func makeWithResults(results: Results) -> (
31 | ResultsTableViewModel,
32 | effects: SignalProducer)
33 | {
34 | let (effects, effectsSink) = SignalProducer.buffer(1)
35 |
36 | return (
37 | ResultsTableViewModel(
38 | results: results,
39 | effectsSink: effectsSink),
40 | effects: effects)
41 | }
42 |
43 | func numberOfRows() -> Int {
44 | return countElements(resultsTable.rows)
45 | }
46 |
47 | func cellForRow(row: Int) -> Cell {
48 | switch resultsTable.rows[row] {
49 | case let .Item(text: text, clickURL: _):
50 | return .Selectable(text)
51 | case .Separator:
52 | return .Separator
53 | }
54 | }
55 |
56 | // MARK: private
57 |
58 | private let resultsTable: ResultsTable
59 |
60 | private init(results: Results, effectsSink: Signal.Observer) {
61 | let resultsTable = ResultsTable(results: results)
62 | self.resultsTable = resultsTable
63 |
64 | didRecognizeActionOnRowAtIndex = {
65 | let buffer = SignalProducer<(Action, Int), NoError>.buffer(1)
66 | buffer.0
67 | |> mapOptional { (action, index) in
68 | switch resultsTable.rows[index] {
69 | case let .Item(text: text, clickURL: clickURL):
70 | switch action {
71 | case .Click:
72 | return .OpenURL(clickURL)
73 | case .Copy:
74 | let pasteboardItem = pasteboardItemForLinkWithText(text, URL: clickURL)
75 | return .WriteObjectsToPasteboard([pasteboardItem])
76 | }
77 | case .Separator:
78 | return nil
79 | }
80 | }
81 | |> start(effectsSink)
82 | return buffer.1
83 | }()
84 |
85 | }
86 | }
87 |
88 | private struct ResultsTable {
89 | enum Row {
90 | case Item(text: String, clickURL: NSURL)
91 | case Separator
92 | }
93 |
94 | let rows: [Row]
95 |
96 | init () {
97 | rows = []
98 | }
99 |
100 | init(results: Results) {
101 | let sections: [[Row]] = [results.projects, results.tasks]
102 | .filter { !$0.isEmpty }
103 | .map { section in
104 | section.map { result in
105 | .Item(
106 | text: result.name.stringByReplacingOccurrencesOfString("\n", withString: "⏎"),
107 | clickURL: result.URL)
108 | }}
109 |
110 | rows = [.Separator].join(sections)
111 | }
112 | }
113 |
114 | private func pasteboardItemForLinkWithText(text: String, #URL: NSURL) -> NSPasteboardItem {
115 | let hyperlink = NSAttributedString(
116 | string: text,
117 | attributes: [NSLinkAttributeName: URL])
118 |
119 | let item = NSPasteboardItem()
120 | item.setData(
121 | hyperlink.RTFFromRange(
122 | NSMakeRange(0, hyperlink.length),
123 | documentAttributes: nil ),
124 | forType: NSPasteboardTypeRTF)
125 | item.setString(
126 | URL.absoluteString,
127 | forType: NSPasteboardTypeString)
128 |
129 | return item
130 | }
131 |
--------------------------------------------------------------------------------
/Sukhasana/ViewModel/MainScreenModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainScreenModel.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/8/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import ReactiveCocoa
10 |
11 | struct MainScreenModel {
12 | enum Effect {
13 | case OpenSettings
14 | case Results(ResultsTableViewModel.Effect)
15 | }
16 |
17 | let textFieldText = MutableProperty("")
18 | let activityIndicatorIsAnimating: PropertyOf
19 | let resultsTableViewModel: PropertyOf
20 | let didClickSettingsButton: Signal<(), NoError>.Observer
21 |
22 | static func makeWithSettings(settings: Settings) -> (
23 | MainScreenModel,
24 | effects: SignalProducer)
25 | {
26 | let (effects, effectsSink) = SignalProducer.buffer(1)
27 |
28 | return (
29 | MainScreenModel(
30 | settings: settings,
31 | effectsSink: effectsSink),
32 | effects: effects
33 | )
34 | }
35 |
36 | // MARK: private
37 |
38 | private init(
39 | settings: Settings,
40 | effectsSink: Signal.Observer)
41 | {
42 | let client = APIClient(APIKey: settings.APIKey)
43 |
44 | let fetchState: SignalProducer = textFieldText.producer
45 | |> map { query -> SignalProducer in
46 | if query == "" {
47 | // The empty query always returns no results, so don't bother
48 | return SignalProducer(value: Results(projects: [], tasks: []))
49 | } else {
50 | let request = { type in
51 | client.requestTypeaheadResultsInWorkspace(settings.workspaceID, ofType: type, matchingQuery: query)
52 | |> map(resultsFromJSON)
53 | }
54 | return zip(request(.Project), request(.Task))
55 | |> map { projects, tasks in Results(projects: projects, tasks: tasks) }
56 | }
57 | }
58 | |> map { request in
59 | request
60 | |> map { results in .Fetched(results) }
61 | |> catchTo(.Failed)
62 | |> startWith(.Fetching)
63 | }
64 | |> join(.Latest)
65 | |> replay(capacity: 1)
66 |
67 | let resultsTableViewModelsAndEffects: SignalProducer<(ResultsTableViewModel, SignalProducer), NoError> =
68 | fetchState
69 | |> map { fetchState in
70 | switch fetchState {
71 | case .Fetched(let results):
72 | return ResultsTableViewModel.makeWithResults(results)
73 | case .Initial, .Failed, .Fetching:
74 | return ResultsTableViewModel.makeWithResults(Results.empty)
75 | }
76 | }
77 | |> replay(capacity: 1)
78 |
79 | resultsTableViewModel = propertyOf(
80 | ResultsTableViewModel.makeWithResults(Results.empty).0,
81 | resultsTableViewModelsAndEffects |> map { $0.0 } )
82 |
83 | resultsTableViewModelsAndEffects
84 | |> joinMap(.Latest) { $0.1 }
85 | |> map { .Results($0) }
86 | |> start(effectsSink)
87 |
88 | activityIndicatorIsAnimating = propertyOf(false, fetchState
89 | |> map { resultsState in
90 | switch resultsState {
91 | case .Fetching:
92 | return true
93 | case .Initial, .Failed, .Fetched:
94 | return false
95 | }
96 | })
97 |
98 | didClickSettingsButton = {
99 | let buffer = SignalProducer<(), NoError>.buffer(1)
100 | buffer.0
101 | |> map { _ in .OpenSettings}
102 | |> start(effectsSink)
103 | return buffer.1
104 | }()
105 | }
106 | }
107 |
108 | private enum FetchState {
109 | case Initial
110 | case Fetching
111 | case Failed
112 | case Fetched(Results)
113 | }
114 |
115 | private func resultsFromJSON(JSON: NSDictionary) -> [Result] {
116 | var names = [Result]()
117 | if let data = JSON["data"] as? Array> {
118 | for object in data {
119 | if let name = object["name"] as? String {
120 | if let id = object["id"] as? NSNumber {
121 | names.append(Result(name: name, id: id.stringValue))
122 | }
123 | }
124 | }
125 | }
126 | return names
127 | }
128 |
129 |
--------------------------------------------------------------------------------
/Sukhasana.xcodeproj/xcshareddata/xcschemes/Sukhasana.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
75 |
77 |
83 |
84 |
85 |
86 |
87 |
88 |
94 |
96 |
102 |
103 |
104 |
105 |
107 |
108 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/Sukhasana/ViewModel/SettingsScreenModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsScreenModel.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/16/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import ReactiveCocoa
10 |
11 | struct SettingsScreenModel {
12 | let shortcutViewAssociatedUserDefaultsKey: String
13 | let APIKeyTextFieldText: MutableProperty
14 | let saveButtonEnabled, workspacePopUpButtonEnabled, progressIndicatorAnimating: PropertyOf
15 | let workspacePopUpItemsTitles: PropertyOf<[String]>
16 | let workspacePopupSelectedIndex: PropertyOf
17 | let didClickSaveButton: Signal<(), NoError>.Observer
18 | let workspacePopUpDidSelectItemAtIndex: Signal.Observer
19 |
20 | static func makeWithSettings(settings: Settings?, globalShortcutDefaultsKey: String) -> (SettingsScreenModel, didSaveSettings: SignalProducer) {
21 | let (didSaveSettings, didSaveSettingsSink) = SignalProducer.buffer(1)
22 |
23 | return (
24 | SettingsScreenModel(
25 | settings: settings,
26 | globalShortcutDefaultsKey: globalShortcutDefaultsKey,
27 | didSaveSettingsSink: didSaveSettingsSink),
28 | didSaveSettings: didSaveSettings)
29 | }
30 |
31 | // MARK: private
32 |
33 | private init(
34 | settings: Settings?,
35 | globalShortcutDefaultsKey: String,
36 | didSaveSettingsSink: Signal.Observer)
37 | {
38 | shortcutViewAssociatedUserDefaultsKey = globalShortcutDefaultsKey
39 | APIKeyTextFieldText = MutableProperty(settings?.APIKey ?? "")
40 |
41 | let workspacesState: SignalProducer = APIKeyTextFieldText.producer
42 | |> map { APIClient(APIKey: $0) }
43 | |> map { $0.requestWorkspaces() }
44 | |> map { $0 |> map(workspacesFromJSON) }
45 | |> map { $0
46 | |> map { .Fetched($0) }
47 | |> catchTo(.Failed)
48 | |> startWith(.Fetching)
49 | }
50 | |> join(.Latest)
51 | |> replay(capacity: 1)
52 |
53 | workspacePopUpButtonEnabled = propertyOf(false, workspacesState
54 | |> map { switch $0 {
55 | case .Fetched(let workspaces):
56 | return !isEmpty(workspaces)
57 | case .Initial, .Fetching, .Failed:
58 | return false
59 | }
60 | })
61 |
62 | saveButtonEnabled = workspacePopUpButtonEnabled
63 |
64 | progressIndicatorAnimating = propertyOf(false, workspacesState
65 | |> map { switch $0 {
66 | case .Fetching:
67 | return true
68 | case .Initial, .Fetched, .Failed:
69 | return false
70 | }
71 | })
72 |
73 | workspacePopUpItemsTitles = propertyOf([], workspacesState
74 | |> map { switch $0 {
75 | case .Fetched(let workspaces):
76 | return isEmpty(workspaces)
77 | ? ["(no workspaces)"]
78 | : workspaces.map { $0.name }
79 | case .Initial, .Fetching, .Failed:
80 | // Placeholder text for when popup is disabled
81 | return ["Workspace"]
82 | }
83 | })
84 |
85 | let (workspaceSelectedIndexes, workspaceSelectedIndexSink) = SignalProducer.buffer(1)
86 | workspacePopUpDidSelectItemAtIndex = workspaceSelectedIndexSink
87 |
88 | workspacePopupSelectedIndex = propertyOf(0,
89 | SignalProducer(values: [
90 | workspaceSelectedIndexes,
91 | workspacePopUpItemsTitles.producer |> map { _ in 0 }
92 | ])
93 | |> join(.Merge)
94 | )
95 |
96 | let (didClickSaveButtonProducer, didClickSaveButtonSink) = SignalProducer<(), NoError>.buffer(1)
97 | didClickSaveButton = didClickSaveButtonSink
98 | combineLatest(workspacesState, workspacePopupSelectedIndex.producer, APIKeyTextFieldText.producer)
99 | |> sampleOn(didClickSaveButtonProducer)
100 | |> map { workspacesState, workspacePopupSelectedIndex, APIKeyTextFieldText in
101 | switch workspacesState {
102 | case .Fetched(let workspaces):
103 | return Settings(
104 | APIKey: APIKeyTextFieldText,
105 | workspaceID: workspaces[workspacePopupSelectedIndex].id)
106 | default:
107 | fatalError("can't save with no workspaces loaded")
108 | }}
109 | |> start(didSaveSettingsSink)
110 |
111 | }
112 | }
113 |
114 | private struct Workspace {
115 | let id, name: String
116 | }
117 |
118 | private enum WorkspacesState {
119 | case Initial
120 | case Fetching
121 | case Failed
122 | case Fetched([Workspace])
123 | }
124 |
125 | private func workspacesFromJSON(JSON: NSDictionary) -> [Workspace] {
126 | var workspaces = [Workspace]()
127 | if let data = JSON["data"] as? Array> {
128 | for object in data {
129 | if let name = object["name"] as? String {
130 | if let id = object["id"] as? NSNumber {
131 | workspaces.append(Workspace(id: id.stringValue, name: name))
132 | }
133 | }
134 | }
135 | }
136 | return workspaces
137 | }
138 |
--------------------------------------------------------------------------------
/Sukhasana/View/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/4/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import MASShortcut
11 |
12 | @NSApplicationMain
13 | class AppDelegate: NSObject, MainViewControllerDelegate, NSApplicationDelegate, NSWindowDelegate {
14 | @IBOutlet var panel: NSPanel!
15 |
16 | deinit {
17 | notificationCenter.removeObserver(statusItemMoveObserver)
18 | }
19 |
20 | @IBAction func didClickStatusItem(sender: AnyObject) {
21 | if panel.keyWindow {
22 | panel.close()
23 | } else {
24 | // Later in this event loop iteration, the highlight of the status item
25 | // button is automatically set to false. So if we want to highlight the
26 | // button, as we do in `windowDidBecomeKey`, we need to ensure that happens
27 | // in a later iteration.
28 | dispatch_async(dispatch_get_main_queue()) {
29 | self.panel.makeKeyAndOrderFront(self)
30 | }
31 | }
32 | }
33 |
34 | // MARK: MainViewControllerDelegate
35 |
36 | func mainViewControllerDidChangeFittingSize(mainViewController: MainViewController) {
37 | updatePanelFrame()
38 | }
39 |
40 | // MARK: NSWindowDelegate
41 |
42 | func windowDidResignKey(notification: NSNotification) {
43 | // windowDidResignKey is always called when the window is closed (e.g., by
44 | // calling panel.close()), so it's a reliable place to un-highlight the
45 | // status item.
46 | statusItem.button?.highlight(false)
47 |
48 | // However, the window isn't necessarily closed when it resigns key, so we
49 | // must ensure here that it closes.
50 | panel.close()
51 | }
52 |
53 | func windowDidBecomeKey(notification: NSNotification) {
54 | displayingViewController?.viewDidDisplay()
55 | self.statusItem.button?.highlight(true)
56 | }
57 |
58 | // MARK: NSApplicationDelegate
59 |
60 | func applicationDidFinishLaunching(notification: NSNotification) {
61 | statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2 /* NSSquareStatusItemLength */)
62 | if let button = statusItem.button {
63 | button.image = NSImage(named: "StatusItemIcon")
64 | button.alternateImage = NSImage(named: "StatusItemIconHighlighted")
65 | button.target = self
66 | button.action = "didClickStatusItem:"
67 | }
68 |
69 | panel.floatingPanel = true
70 |
71 | // The status item can move when other items are removed or hidden.
72 | statusItemMoveObserver = notificationCenter.addObserverForName(
73 | NSWindowDidMoveNotification,
74 | object: statusItem.button!.window!,
75 | queue: nil,
76 | usingBlock: { [weak self] _ in self?.updatePanelFrame(); return })
77 |
78 | model.effects.start { [weak self] effect in
79 | if let _self = self {
80 | switch effect {
81 |
82 | case .DisplayScreen(let screen):
83 | _self.displayingViewController = {
84 | switch screen {
85 | case .Settings(let model):
86 | return SettingsViewController(model: model)
87 | case .Main(let model):
88 | return MainViewController(model: model, delegate: _self)
89 | }
90 | }()
91 | _self.setContentView(_self.displayingViewController!.view)
92 | _self.displayingViewController!.viewDidDisplay()
93 |
94 | case .Results(.OpenURL(let URL)):
95 | NSWorkspace.sharedWorkspace().openURL(URL)
96 | _self.panel.close()
97 |
98 | case .Results(.WriteObjectsToPasteboard(let objects)):
99 | let pasteboard = NSPasteboard.generalPasteboard()
100 | pasteboard.clearContents()
101 | pasteboard.writeObjects(objects)
102 | _self.panel.close()
103 | }
104 | }
105 | }
106 |
107 | if model.shouldOpenPanelOnLaunch {
108 | panel.makeKeyAndOrderFront(self)
109 | }
110 |
111 | MASShortcutBinder.sharedBinder().bindShortcutWithDefaultsKey(globalShortcutDefaultsKey) { [weak self] in
112 | if let _self = self {
113 | _self.panel.makeKeyAndOrderFront(_self)
114 | }
115 | }
116 | }
117 |
118 | // MARK: private
119 |
120 | private func setContentView(view: NSView) {
121 | panel.contentView = view
122 | updatePanelFrame()
123 | }
124 |
125 | private func updatePanelFrame() {
126 | if let statusItemWindow = statusItem.button?.window? {
127 | let statusItemFrame = statusItemWindow.frame
128 | let panelSize = panel.contentView.fittingSize
129 | let maxPanelOriginX: CGFloat? = {
130 | if let screen = statusItemWindow.screen {
131 | let frame = screen.visibleFrame
132 | return frame.origin.x + frame.size.width - panelSize.width
133 | } else {
134 | return nil
135 | }
136 | }()
137 | let panelOrigin = CGPoint(
138 | x: min(statusItemFrame.origin.x, maxPanelOriginX ?? CGFloat.max),
139 | y: statusItemFrame.origin.y + statusItemFrame.size.height)
140 | let panelFrame = NSMakeRect(
141 | panelOrigin.x,
142 | panelOrigin.y - panelSize.height,
143 | panelSize.width,
144 | panelSize.height)
145 |
146 | panel.setFrame(panelFrame, display: true)
147 | }
148 | }
149 |
150 | private let model = ApplicationModel(
151 | settingsStore: NSUserDefaults.standardUserDefaults(),
152 | globalShortcutDefaultsKey: globalShortcutDefaultsKey)
153 | private let notificationCenter = NSNotificationCenter.defaultCenter()
154 | private var statusItem: NSStatusItem!
155 | private var statusItemMoveObserver: AnyObject!
156 | private var displayingViewController: ViewController?
157 | }
158 |
159 | private let globalShortcutDefaultsKey = "globalShortcut"
160 |
161 |
--------------------------------------------------------------------------------
/Sukhasana/View/ResultsTableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableView.swift
3 | // Sukhasana
4 | //
5 | // Created by Tom Brow on 2/22/15.
6 | // Copyright (c) 2015 Tom Brow. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Carbon
11 | import ReactiveCocoa
12 |
13 | class ResultsTableView: NSTableView, NSTableViewDataSource, NSTableViewDelegate {
14 | let model: MutableProperty
15 | let fittingHeight: SignalProducer
16 |
17 | required init?(coder: NSCoder) {
18 | model = MutableProperty(ResultsTableViewModel.makeWithResults(Results.empty).0)
19 |
20 | fittingHeight = model.producer
21 | |> map { model in
22 | let numberOfRows = model.numberOfRows()
23 | let bottomPadding = CGFloat(numberOfRows > 0 ? 4 : 0)
24 | return bottomPadding +
25 | map(0..= 0 {
51 | // For some reason, the flashHighlightedRowsThen animation's timing is off
52 | // if initiated in the same event loop iteration `self.copy` is called.
53 | dispatch_async(dispatch_get_main_queue()) {
54 | self.didRecognizeAction(.Copy, onRowAtIndex: self.selectedRow)
55 | }
56 | }
57 | }
58 |
59 | func didRecognizeAction(action: ResultsTableViewModel.Action, onRowAtIndex index: Int) {
60 | flashHighlightedRowsThen {
61 | sendNext(self.model.value.didRecognizeActionOnRowAtIndex, (action, index))
62 | }
63 | }
64 |
65 | // MARK: NSTableViewDataSource
66 |
67 | func numberOfRowsInTableView(tableView: NSTableView) -> Int {
68 | return model.value.numberOfRows()
69 | }
70 |
71 | // MARK: NSTableViewDelegate
72 |
73 | func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
74 | switch model.value.cellForRow(row) {
75 | case .Selectable(let text):
76 | let view = tableView.makeViewWithIdentifier("SelectableCell", owner: self) as NSTableCellView
77 | view.textField?.stringValue = text
78 | return view
79 | case .Separator:
80 | return tableView.makeViewWithIdentifier("SeparatorView", owner: self) as? NSView
81 | }
82 | }
83 |
84 | func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
85 | switch model.value.cellForRow(row) {
86 | case .Selectable:
87 | return true
88 | case .Separator:
89 | return false
90 | }
91 | }
92 |
93 | func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
94 | return heightForCell(model.value.cellForRow(row))
95 | }
96 |
97 | // MARK: NSResponder
98 |
99 | func acceptsFirstResponder() -> Bool {
100 | return numberOfRows > 0
101 | }
102 |
103 | override func becomeFirstResponder() -> Bool {
104 | selectRowIndexes(NSIndexSet(index: 0), byExtendingSelection: false)
105 | return true
106 | }
107 |
108 | override func mouseExited(theEvent: NSEvent) {
109 | super.mouseExited(theEvent)
110 | selectRowIndexes(NSIndexSet(), byExtendingSelection: false)
111 | }
112 |
113 | override func mouseMoved(theEvent: NSEvent) {
114 | super.mouseMoved(theEvent)
115 |
116 | let row = rowAtPoint(convertPoint(theEvent.locationInWindow, fromView: nil))
117 | let rowIndexesToSelect = row == -1
118 | ? NSIndexSet()
119 | : NSIndexSet(index: row)
120 |
121 | window?.makeFirstResponder(self)
122 | selectRowIndexes(rowIndexesToSelect, byExtendingSelection: false)
123 | }
124 |
125 | override func mouseDown(theEvent: NSEvent) {
126 | super.mouseDown(theEvent)
127 |
128 | let row = rowAtPoint(convertPoint(theEvent.locationInWindow, fromView: nil))
129 | if row >= 0 {
130 | didRecognizeAction(.Click, onRowAtIndex: row)
131 | }
132 | }
133 |
134 | override func keyDown(theEvent: NSEvent) {
135 | switch Int(theEvent.keyCode) {
136 | case kVK_UpArrow where selectedRow == 0:
137 | window?.selectKeyViewPrecedingView(self)
138 | case kVK_Space, kVK_Return where selectedRow >= 0:
139 | didRecognizeAction(.Click, onRowAtIndex: selectedRow)
140 | default:
141 | super.keyDown(theEvent)
142 | }
143 | }
144 |
145 | override func resignFirstResponder() -> Bool {
146 | if super.resignFirstResponder() {
147 | deselectAll(self)
148 | return true
149 | } else {
150 | return false
151 | }
152 | }
153 | }
154 |
155 | private func heightForCell(cell: ResultsTableViewModel.Cell) -> CGFloat {
156 | switch cell {
157 | case .Selectable:
158 | return 22
159 | case .Separator:
160 | return 1
161 | }
162 | }
163 |
164 | extension NSTableView {
165 | func flashHighlightedRowsThen(callback: () -> ()) {
166 | // Simulate NSMenu's flash on selecting a row, not without jank
167 | let baseHighlightStyle = selectionHighlightStyle
168 | let mainQueue = dispatch_get_main_queue()
169 | let toggleInterval = Int64(NSEC_PER_SEC) / 20
170 | let numberOfToggles: Int64 = 3
171 |
172 | for i: Int64 in 0...numberOfToggles {
173 | let time = dispatch_time(DISPATCH_TIME_NOW, toggleInterval * i)
174 | dispatch_after(time, mainQueue) {
175 | if i < numberOfToggles {
176 | self.selectionHighlightStyle = i % 2 == 0
177 | ? NSTableViewSelectionHighlightStyle.None
178 | : baseHighlightStyle
179 | } else {
180 | self.selectionHighlightStyle = baseHighlightStyle
181 | callback()
182 | }
183 | }
184 | }
185 | }
186 | }
--------------------------------------------------------------------------------
/Sukhasana/View/SettingsViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/Sukhasana/View/MainViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/Sukhasana.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | EA037E691A834B9300F47B17 /* SukhasanaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA037E681A834B9300F47B17 /* SukhasanaTests.swift */; };
11 | EA037E7F1A847CF800F47B17 /* LlamaKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E791A847CF800F47B17 /* LlamaKit.framework */; };
12 | EA037E801A847CF800F47B17 /* LlamaKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E791A847CF800F47B17 /* LlamaKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
13 | EA037E851A847CF800F47B17 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */; };
14 | EA037E861A847CF800F47B17 /* ReactiveCocoa.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
15 | EA125B1A1AA40426004177C4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EA125B031AA40426004177C4 /* Images.xcassets */; };
16 | EA125B1C1AA40426004177C4 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B061AA40426004177C4 /* Settings.swift */; };
17 | EA125B1D1AA40426004177C4 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B081AA40426004177C4 /* APIClient.swift */; };
18 | EA125B1E1AA40426004177C4 /* NSUserDefaults+SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B091AA40426004177C4 /* NSUserDefaults+SettingsStore.swift */; };
19 | EA125B1F1AA40426004177C4 /* SignalOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B0A1AA40426004177C4 /* SignalOperators.swift */; };
20 | EA125B201AA40426004177C4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B0C1AA40426004177C4 /* AppDelegate.swift */; };
21 | EA125B211AA40426004177C4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = EA125B0D1AA40426004177C4 /* MainMenu.xib */; };
22 | EA125B221AA40426004177C4 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B0E1AA40426004177C4 /* MainViewController.swift */; };
23 | EA125B231AA40426004177C4 /* MainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = EA125B0F1AA40426004177C4 /* MainViewController.xib */; };
24 | EA125B241AA40426004177C4 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B101AA40426004177C4 /* Panel.swift */; };
25 | EA125B251AA40426004177C4 /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B111AA40426004177C4 /* SeparatorView.swift */; };
26 | EA125B261AA40426004177C4 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B121AA40426004177C4 /* SettingsViewController.swift */; };
27 | EA125B271AA40426004177C4 /* SettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = EA125B131AA40426004177C4 /* SettingsViewController.xib */; };
28 | EA125B281AA40426004177C4 /* ResultsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B141AA40426004177C4 /* ResultsTableView.swift */; };
29 | EA125B291AA40426004177C4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B151AA40426004177C4 /* ViewController.swift */; };
30 | EA125B2A1AA40426004177C4 /* ApplicationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B171AA40426004177C4 /* ApplicationModel.swift */; };
31 | EA125B2B1AA40426004177C4 /* MainScreenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B181AA40426004177C4 /* MainScreenModel.swift */; };
32 | EA125B2C1AA40426004177C4 /* SettingsScreenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA125B191AA40426004177C4 /* SettingsScreenModel.swift */; };
33 | EA307A601A87FC780008A566 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA307A5F1A87FC780008A566 /* Alamofire.framework */; };
34 | EA307A611A87FC780008A566 /* Alamofire.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EA307A5F1A87FC780008A566 /* Alamofire.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
35 | EAFD89FB1ABF7A0400DA0CBE /* ResultsTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD89FA1ABF7A0400DA0CBE /* ResultsTableViewModel.swift */; };
36 | EAFD89FD1ABF7E3100DA0CBE /* Results.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD89FC1ABF7E3100DA0CBE /* Results.swift */; };
37 | EAFFDC7D1AACF5C400F38834 /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */; };
38 | EAFFDC7E1AACF5C400F38834 /* MASShortcut.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
39 | /* End PBXBuildFile section */
40 |
41 | /* Begin PBXContainerItemProxy section */
42 | EA037E631A834B9300F47B17 /* PBXContainerItemProxy */ = {
43 | isa = PBXContainerItemProxy;
44 | containerPortal = EA037E4A1A834B9300F47B17 /* Project object */;
45 | proxyType = 1;
46 | remoteGlobalIDString = EA037E511A834B9300F47B17;
47 | remoteInfo = Sukhasana;
48 | };
49 | /* End PBXContainerItemProxy section */
50 |
51 | /* Begin PBXCopyFilesBuildPhase section */
52 | EA037E871A847CF800F47B17 /* Embed Frameworks */ = {
53 | isa = PBXCopyFilesBuildPhase;
54 | buildActionMask = 2147483647;
55 | dstPath = "";
56 | dstSubfolderSpec = 10;
57 | files = (
58 | EA037E801A847CF800F47B17 /* LlamaKit.framework in Embed Frameworks */,
59 | EA037E861A847CF800F47B17 /* ReactiveCocoa.framework in Embed Frameworks */,
60 | EAFFDC7E1AACF5C400F38834 /* MASShortcut.framework in Embed Frameworks */,
61 | EA307A611A87FC780008A566 /* Alamofire.framework in Embed Frameworks */,
62 | );
63 | name = "Embed Frameworks";
64 | runOnlyForDeploymentPostprocessing = 0;
65 | };
66 | /* End PBXCopyFilesBuildPhase section */
67 |
68 | /* Begin PBXFileReference section */
69 | EA037E521A834B9300F47B17 /* Sukhasana.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sukhasana.app; sourceTree = BUILT_PRODUCTS_DIR; };
70 | EA037E621A834B9300F47B17 /* SukhasanaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SukhasanaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
71 | EA037E671A834B9300F47B17 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
72 | EA037E681A834B9300F47B17 /* SukhasanaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SukhasanaTests.swift; sourceTree = ""; };
73 | EA037E791A847CF800F47B17 /* LlamaKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LlamaKit.framework; path = ../Carthage/Build/Mac/LlamaKit.framework; sourceTree = ""; };
74 | EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveCocoa.framework; path = ../Carthage/Build/Mac/ReactiveCocoa.framework; sourceTree = ""; };
75 | EA125B031AA40426004177C4 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
76 | EA125B041AA40426004177C4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
77 | EA125B061AA40426004177C4 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; };
78 | EA125B081AA40426004177C4 /* APIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; };
79 | EA125B091AA40426004177C4 /* NSUserDefaults+SettingsStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+SettingsStore.swift"; sourceTree = ""; };
80 | EA125B0A1AA40426004177C4 /* SignalOperators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalOperators.swift; sourceTree = ""; };
81 | EA125B0C1AA40426004177C4 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
82 | EA125B0D1AA40426004177C4 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; };
83 | EA125B0E1AA40426004177C4 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; };
84 | EA125B0F1AA40426004177C4 /* MainViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainViewController.xib; sourceTree = ""; };
85 | EA125B101AA40426004177C4 /* Panel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Panel.swift; sourceTree = ""; };
86 | EA125B111AA40426004177C4 /* SeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; };
87 | EA125B121AA40426004177C4 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; };
88 | EA125B131AA40426004177C4 /* SettingsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsViewController.xib; sourceTree = ""; };
89 | EA125B141AA40426004177C4 /* ResultsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultsTableView.swift; sourceTree = ""; };
90 | EA125B151AA40426004177C4 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
91 | EA125B171AA40426004177C4 /* ApplicationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationModel.swift; sourceTree = ""; };
92 | EA125B181AA40426004177C4 /* MainScreenModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainScreenModel.swift; sourceTree = ""; };
93 | EA125B191AA40426004177C4 /* SettingsScreenModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsScreenModel.swift; sourceTree = ""; };
94 | EA307A5F1A87FC780008A566 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Alamofire.framework; path = ../Carthage/Build/Mac/Alamofire.framework; sourceTree = ""; };
95 | EAFD89FA1ABF7A0400DA0CBE /* ResultsTableViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultsTableViewModel.swift; sourceTree = ""; };
96 | EAFD89FC1ABF7E3100DA0CBE /* Results.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Results.swift; sourceTree = ""; };
97 | EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MASShortcut.framework; path = ../Carthage/Build/Mac/MASShortcut.framework; sourceTree = ""; };
98 | /* End PBXFileReference section */
99 |
100 | /* Begin PBXFrameworksBuildPhase section */
101 | EA037E4F1A834B9300F47B17 /* Frameworks */ = {
102 | isa = PBXFrameworksBuildPhase;
103 | buildActionMask = 2147483647;
104 | files = (
105 | EA037E7F1A847CF800F47B17 /* LlamaKit.framework in Frameworks */,
106 | EA037E851A847CF800F47B17 /* ReactiveCocoa.framework in Frameworks */,
107 | EAFFDC7D1AACF5C400F38834 /* MASShortcut.framework in Frameworks */,
108 | EA307A601A87FC780008A566 /* Alamofire.framework in Frameworks */,
109 | );
110 | runOnlyForDeploymentPostprocessing = 0;
111 | };
112 | EA037E5F1A834B9300F47B17 /* Frameworks */ = {
113 | isa = PBXFrameworksBuildPhase;
114 | buildActionMask = 2147483647;
115 | files = (
116 | );
117 | runOnlyForDeploymentPostprocessing = 0;
118 | };
119 | /* End PBXFrameworksBuildPhase section */
120 |
121 | /* Begin PBXGroup section */
122 | EA037E491A834B9300F47B17 = {
123 | isa = PBXGroup;
124 | children = (
125 | EA125B021AA40426004177C4 /* Sukhasana */,
126 | EA037E651A834B9300F47B17 /* SukhasanaTests */,
127 | EA037E531A834B9300F47B17 /* Products */,
128 | );
129 | sourceTree = "";
130 | };
131 | EA037E531A834B9300F47B17 /* Products */ = {
132 | isa = PBXGroup;
133 | children = (
134 | EA037E521A834B9300F47B17 /* Sukhasana.app */,
135 | EA037E621A834B9300F47B17 /* SukhasanaTests.xctest */,
136 | );
137 | name = Products;
138 | sourceTree = "";
139 | };
140 | EA037E651A834B9300F47B17 /* SukhasanaTests */ = {
141 | isa = PBXGroup;
142 | children = (
143 | EA037E681A834B9300F47B17 /* SukhasanaTests.swift */,
144 | EA037E661A834B9300F47B17 /* Supporting Files */,
145 | );
146 | path = SukhasanaTests;
147 | sourceTree = "";
148 | };
149 | EA037E661A834B9300F47B17 /* Supporting Files */ = {
150 | isa = PBXGroup;
151 | children = (
152 | EA037E671A834B9300F47B17 /* Info.plist */,
153 | );
154 | name = "Supporting Files";
155 | sourceTree = "";
156 | };
157 | EA125B021AA40426004177C4 /* Sukhasana */ = {
158 | isa = PBXGroup;
159 | children = (
160 | EA125B2D1AA4044A004177C4 /* Frameworks */,
161 | EA125B031AA40426004177C4 /* Images.xcassets */,
162 | EA125B041AA40426004177C4 /* Info.plist */,
163 | EA125B051AA40426004177C4 /* Model */,
164 | EA125B071AA40426004177C4 /* Other */,
165 | EA125B0B1AA40426004177C4 /* View */,
166 | EA125B161AA40426004177C4 /* ViewModel */,
167 | );
168 | path = Sukhasana;
169 | sourceTree = "";
170 | };
171 | EA125B051AA40426004177C4 /* Model */ = {
172 | isa = PBXGroup;
173 | children = (
174 | EA125B061AA40426004177C4 /* Settings.swift */,
175 | EAFD89FC1ABF7E3100DA0CBE /* Results.swift */,
176 | );
177 | path = Model;
178 | sourceTree = "";
179 | };
180 | EA125B071AA40426004177C4 /* Other */ = {
181 | isa = PBXGroup;
182 | children = (
183 | EA125B081AA40426004177C4 /* APIClient.swift */,
184 | EA125B091AA40426004177C4 /* NSUserDefaults+SettingsStore.swift */,
185 | EA125B0A1AA40426004177C4 /* SignalOperators.swift */,
186 | );
187 | path = Other;
188 | sourceTree = "";
189 | };
190 | EA125B0B1AA40426004177C4 /* View */ = {
191 | isa = PBXGroup;
192 | children = (
193 | EA125B0C1AA40426004177C4 /* AppDelegate.swift */,
194 | EA125B0D1AA40426004177C4 /* MainMenu.xib */,
195 | EA125B0E1AA40426004177C4 /* MainViewController.swift */,
196 | EA125B0F1AA40426004177C4 /* MainViewController.xib */,
197 | EA125B101AA40426004177C4 /* Panel.swift */,
198 | EA125B141AA40426004177C4 /* ResultsTableView.swift */,
199 | EA125B111AA40426004177C4 /* SeparatorView.swift */,
200 | EA125B121AA40426004177C4 /* SettingsViewController.swift */,
201 | EA125B131AA40426004177C4 /* SettingsViewController.xib */,
202 | EA125B151AA40426004177C4 /* ViewController.swift */,
203 | );
204 | path = View;
205 | sourceTree = "";
206 | };
207 | EA125B161AA40426004177C4 /* ViewModel */ = {
208 | isa = PBXGroup;
209 | children = (
210 | EA125B171AA40426004177C4 /* ApplicationModel.swift */,
211 | EA125B181AA40426004177C4 /* MainScreenModel.swift */,
212 | EAFD89FA1ABF7A0400DA0CBE /* ResultsTableViewModel.swift */,
213 | EA125B191AA40426004177C4 /* SettingsScreenModel.swift */,
214 | );
215 | path = ViewModel;
216 | sourceTree = "";
217 | };
218 | EA125B2D1AA4044A004177C4 /* Frameworks */ = {
219 | isa = PBXGroup;
220 | children = (
221 | EA307A5F1A87FC780008A566 /* Alamofire.framework */,
222 | EA037E791A847CF800F47B17 /* LlamaKit.framework */,
223 | EAFFDC7C1AACF5C400F38834 /* MASShortcut.framework */,
224 | EA037E7C1A847CF800F47B17 /* ReactiveCocoa.framework */,
225 | );
226 | name = Frameworks;
227 | sourceTree = "";
228 | };
229 | /* End PBXGroup section */
230 |
231 | /* Begin PBXNativeTarget section */
232 | EA037E511A834B9300F47B17 /* Sukhasana */ = {
233 | isa = PBXNativeTarget;
234 | buildConfigurationList = EA037E6C1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "Sukhasana" */;
235 | buildPhases = (
236 | EA037E4E1A834B9300F47B17 /* Sources */,
237 | EA037E4F1A834B9300F47B17 /* Frameworks */,
238 | EA037E501A834B9300F47B17 /* Resources */,
239 | EA037E871A847CF800F47B17 /* Embed Frameworks */,
240 | );
241 | buildRules = (
242 | );
243 | dependencies = (
244 | );
245 | name = Sukhasana;
246 | productName = Sukhasana;
247 | productReference = EA037E521A834B9300F47B17 /* Sukhasana.app */;
248 | productType = "com.apple.product-type.application";
249 | };
250 | EA037E611A834B9300F47B17 /* SukhasanaTests */ = {
251 | isa = PBXNativeTarget;
252 | buildConfigurationList = EA037E6F1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "SukhasanaTests" */;
253 | buildPhases = (
254 | EA037E5E1A834B9300F47B17 /* Sources */,
255 | EA037E5F1A834B9300F47B17 /* Frameworks */,
256 | EA037E601A834B9300F47B17 /* Resources */,
257 | );
258 | buildRules = (
259 | );
260 | dependencies = (
261 | EA037E641A834B9300F47B17 /* PBXTargetDependency */,
262 | );
263 | name = SukhasanaTests;
264 | productName = SukhasanaTests;
265 | productReference = EA037E621A834B9300F47B17 /* SukhasanaTests.xctest */;
266 | productType = "com.apple.product-type.bundle.unit-test";
267 | };
268 | /* End PBXNativeTarget section */
269 |
270 | /* Begin PBXProject section */
271 | EA037E4A1A834B9300F47B17 /* Project object */ = {
272 | isa = PBXProject;
273 | attributes = {
274 | LastUpgradeCheck = 0610;
275 | ORGANIZATIONNAME = "Tom Brow";
276 | TargetAttributes = {
277 | EA037E511A834B9300F47B17 = {
278 | CreatedOnToolsVersion = 6.1.1;
279 | };
280 | EA037E611A834B9300F47B17 = {
281 | CreatedOnToolsVersion = 6.1.1;
282 | TestTargetID = EA037E511A834B9300F47B17;
283 | };
284 | };
285 | };
286 | buildConfigurationList = EA037E4D1A834B9300F47B17 /* Build configuration list for PBXProject "Sukhasana" */;
287 | compatibilityVersion = "Xcode 3.2";
288 | developmentRegion = English;
289 | hasScannedForEncodings = 0;
290 | knownRegions = (
291 | en,
292 | Base,
293 | );
294 | mainGroup = EA037E491A834B9300F47B17;
295 | productRefGroup = EA037E531A834B9300F47B17 /* Products */;
296 | projectDirPath = "";
297 | projectRoot = "";
298 | targets = (
299 | EA037E511A834B9300F47B17 /* Sukhasana */,
300 | EA037E611A834B9300F47B17 /* SukhasanaTests */,
301 | );
302 | };
303 | /* End PBXProject section */
304 |
305 | /* Begin PBXResourcesBuildPhase section */
306 | EA037E501A834B9300F47B17 /* Resources */ = {
307 | isa = PBXResourcesBuildPhase;
308 | buildActionMask = 2147483647;
309 | files = (
310 | EA125B1A1AA40426004177C4 /* Images.xcassets in Resources */,
311 | EA125B211AA40426004177C4 /* MainMenu.xib in Resources */,
312 | EA125B231AA40426004177C4 /* MainViewController.xib in Resources */,
313 | EA125B271AA40426004177C4 /* SettingsViewController.xib in Resources */,
314 | );
315 | runOnlyForDeploymentPostprocessing = 0;
316 | };
317 | EA037E601A834B9300F47B17 /* Resources */ = {
318 | isa = PBXResourcesBuildPhase;
319 | buildActionMask = 2147483647;
320 | files = (
321 | );
322 | runOnlyForDeploymentPostprocessing = 0;
323 | };
324 | /* End PBXResourcesBuildPhase section */
325 |
326 | /* Begin PBXSourcesBuildPhase section */
327 | EA037E4E1A834B9300F47B17 /* Sources */ = {
328 | isa = PBXSourcesBuildPhase;
329 | buildActionMask = 2147483647;
330 | files = (
331 | EA125B1C1AA40426004177C4 /* Settings.swift in Sources */,
332 | EA125B281AA40426004177C4 /* ResultsTableView.swift in Sources */,
333 | EA125B241AA40426004177C4 /* Panel.swift in Sources */,
334 | EA125B251AA40426004177C4 /* SeparatorView.swift in Sources */,
335 | EA125B291AA40426004177C4 /* ViewController.swift in Sources */,
336 | EAFD89FB1ABF7A0400DA0CBE /* ResultsTableViewModel.swift in Sources */,
337 | EA125B1F1AA40426004177C4 /* SignalOperators.swift in Sources */,
338 | EA125B2C1AA40426004177C4 /* SettingsScreenModel.swift in Sources */,
339 | EA125B2A1AA40426004177C4 /* ApplicationModel.swift in Sources */,
340 | EA125B1D1AA40426004177C4 /* APIClient.swift in Sources */,
341 | EAFD89FD1ABF7E3100DA0CBE /* Results.swift in Sources */,
342 | EA125B221AA40426004177C4 /* MainViewController.swift in Sources */,
343 | EA125B201AA40426004177C4 /* AppDelegate.swift in Sources */,
344 | EA125B2B1AA40426004177C4 /* MainScreenModel.swift in Sources */,
345 | EA125B1E1AA40426004177C4 /* NSUserDefaults+SettingsStore.swift in Sources */,
346 | EA125B261AA40426004177C4 /* SettingsViewController.swift in Sources */,
347 | );
348 | runOnlyForDeploymentPostprocessing = 0;
349 | };
350 | EA037E5E1A834B9300F47B17 /* Sources */ = {
351 | isa = PBXSourcesBuildPhase;
352 | buildActionMask = 2147483647;
353 | files = (
354 | EA037E691A834B9300F47B17 /* SukhasanaTests.swift in Sources */,
355 | );
356 | runOnlyForDeploymentPostprocessing = 0;
357 | };
358 | /* End PBXSourcesBuildPhase section */
359 |
360 | /* Begin PBXTargetDependency section */
361 | EA037E641A834B9300F47B17 /* PBXTargetDependency */ = {
362 | isa = PBXTargetDependency;
363 | target = EA037E511A834B9300F47B17 /* Sukhasana */;
364 | targetProxy = EA037E631A834B9300F47B17 /* PBXContainerItemProxy */;
365 | };
366 | /* End PBXTargetDependency section */
367 |
368 | /* Begin XCBuildConfiguration section */
369 | EA037E6A1A834B9300F47B17 /* Debug */ = {
370 | isa = XCBuildConfiguration;
371 | buildSettings = {
372 | ALWAYS_SEARCH_USER_PATHS = NO;
373 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
374 | CLANG_CXX_LIBRARY = "libc++";
375 | CLANG_ENABLE_MODULES = YES;
376 | CLANG_ENABLE_OBJC_ARC = YES;
377 | CLANG_WARN_BOOL_CONVERSION = YES;
378 | CLANG_WARN_CONSTANT_CONVERSION = YES;
379 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
380 | CLANG_WARN_EMPTY_BODY = YES;
381 | CLANG_WARN_ENUM_CONVERSION = YES;
382 | CLANG_WARN_INT_CONVERSION = YES;
383 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
384 | CLANG_WARN_UNREACHABLE_CODE = YES;
385 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
386 | CODE_SIGN_IDENTITY = "-";
387 | COPY_PHASE_STRIP = NO;
388 | ENABLE_STRICT_OBJC_MSGSEND = YES;
389 | GCC_C_LANGUAGE_STANDARD = gnu99;
390 | GCC_DYNAMIC_NO_PIC = NO;
391 | GCC_OPTIMIZATION_LEVEL = 0;
392 | GCC_PREPROCESSOR_DEFINITIONS = (
393 | "DEBUG=1",
394 | "$(inherited)",
395 | );
396 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
397 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
398 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
399 | GCC_WARN_UNDECLARED_SELECTOR = YES;
400 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
401 | GCC_WARN_UNUSED_FUNCTION = YES;
402 | GCC_WARN_UNUSED_VARIABLE = YES;
403 | MACOSX_DEPLOYMENT_TARGET = 10.10;
404 | MTL_ENABLE_DEBUG_INFO = YES;
405 | ONLY_ACTIVE_ARCH = YES;
406 | SDKROOT = macosx;
407 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
408 | };
409 | name = Debug;
410 | };
411 | EA037E6B1A834B9300F47B17 /* Release */ = {
412 | isa = XCBuildConfiguration;
413 | buildSettings = {
414 | ALWAYS_SEARCH_USER_PATHS = NO;
415 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
416 | CLANG_CXX_LIBRARY = "libc++";
417 | CLANG_ENABLE_MODULES = YES;
418 | CLANG_ENABLE_OBJC_ARC = YES;
419 | CLANG_WARN_BOOL_CONVERSION = YES;
420 | CLANG_WARN_CONSTANT_CONVERSION = YES;
421 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
422 | CLANG_WARN_EMPTY_BODY = YES;
423 | CLANG_WARN_ENUM_CONVERSION = YES;
424 | CLANG_WARN_INT_CONVERSION = YES;
425 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
426 | CLANG_WARN_UNREACHABLE_CODE = YES;
427 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
428 | CODE_SIGN_IDENTITY = "-";
429 | COPY_PHASE_STRIP = YES;
430 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
431 | ENABLE_NS_ASSERTIONS = NO;
432 | ENABLE_STRICT_OBJC_MSGSEND = YES;
433 | GCC_C_LANGUAGE_STANDARD = gnu99;
434 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
435 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
436 | GCC_WARN_UNDECLARED_SELECTOR = YES;
437 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
438 | GCC_WARN_UNUSED_FUNCTION = YES;
439 | GCC_WARN_UNUSED_VARIABLE = YES;
440 | MACOSX_DEPLOYMENT_TARGET = 10.10;
441 | MTL_ENABLE_DEBUG_INFO = NO;
442 | SDKROOT = macosx;
443 | };
444 | name = Release;
445 | };
446 | EA037E6D1A834B9300F47B17 /* Debug */ = {
447 | isa = XCBuildConfiguration;
448 | buildSettings = {
449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
450 | COMBINE_HIDPI_IMAGES = YES;
451 | FRAMEWORK_SEARCH_PATHS = (
452 | "$(inherited)",
453 | "$(PROJECT_DIR)/Carthage/Build/Mac",
454 | "$(PROJECT_DIR)/Sukhasana",
455 | );
456 | INFOPLIST_FILE = Sukhasana/Info.plist;
457 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
458 | PRODUCT_NAME = "$(TARGET_NAME)";
459 | };
460 | name = Debug;
461 | };
462 | EA037E6E1A834B9300F47B17 /* Release */ = {
463 | isa = XCBuildConfiguration;
464 | buildSettings = {
465 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
466 | COMBINE_HIDPI_IMAGES = YES;
467 | FRAMEWORK_SEARCH_PATHS = (
468 | "$(inherited)",
469 | "$(PROJECT_DIR)/Carthage/Build/Mac",
470 | "$(PROJECT_DIR)/Sukhasana",
471 | );
472 | INFOPLIST_FILE = Sukhasana/Info.plist;
473 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
474 | PRODUCT_NAME = "$(TARGET_NAME)";
475 | };
476 | name = Release;
477 | };
478 | EA037E701A834B9300F47B17 /* Debug */ = {
479 | isa = XCBuildConfiguration;
480 | buildSettings = {
481 | BUNDLE_LOADER = "$(TEST_HOST)";
482 | COMBINE_HIDPI_IMAGES = YES;
483 | FRAMEWORK_SEARCH_PATHS = (
484 | "$(DEVELOPER_FRAMEWORKS_DIR)",
485 | "$(inherited)",
486 | );
487 | GCC_PREPROCESSOR_DEFINITIONS = (
488 | "DEBUG=1",
489 | "$(inherited)",
490 | );
491 | INFOPLIST_FILE = SukhasanaTests/Info.plist;
492 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
493 | PRODUCT_NAME = "$(TARGET_NAME)";
494 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sukhasana.app/Contents/MacOS/Sukhasana";
495 | };
496 | name = Debug;
497 | };
498 | EA037E711A834B9300F47B17 /* Release */ = {
499 | isa = XCBuildConfiguration;
500 | buildSettings = {
501 | BUNDLE_LOADER = "$(TEST_HOST)";
502 | COMBINE_HIDPI_IMAGES = YES;
503 | FRAMEWORK_SEARCH_PATHS = (
504 | "$(DEVELOPER_FRAMEWORKS_DIR)",
505 | "$(inherited)",
506 | );
507 | INFOPLIST_FILE = SukhasanaTests/Info.plist;
508 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
509 | PRODUCT_NAME = "$(TARGET_NAME)";
510 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sukhasana.app/Contents/MacOS/Sukhasana";
511 | };
512 | name = Release;
513 | };
514 | /* End XCBuildConfiguration section */
515 |
516 | /* Begin XCConfigurationList section */
517 | EA037E4D1A834B9300F47B17 /* Build configuration list for PBXProject "Sukhasana" */ = {
518 | isa = XCConfigurationList;
519 | buildConfigurations = (
520 | EA037E6A1A834B9300F47B17 /* Debug */,
521 | EA037E6B1A834B9300F47B17 /* Release */,
522 | );
523 | defaultConfigurationIsVisible = 0;
524 | defaultConfigurationName = Release;
525 | };
526 | EA037E6C1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "Sukhasana" */ = {
527 | isa = XCConfigurationList;
528 | buildConfigurations = (
529 | EA037E6D1A834B9300F47B17 /* Debug */,
530 | EA037E6E1A834B9300F47B17 /* Release */,
531 | );
532 | defaultConfigurationIsVisible = 0;
533 | defaultConfigurationName = Release;
534 | };
535 | EA037E6F1A834B9300F47B17 /* Build configuration list for PBXNativeTarget "SukhasanaTests" */ = {
536 | isa = XCConfigurationList;
537 | buildConfigurations = (
538 | EA037E701A834B9300F47B17 /* Debug */,
539 | EA037E711A834B9300F47B17 /* Release */,
540 | );
541 | defaultConfigurationIsVisible = 0;
542 | defaultConfigurationName = Release;
543 | };
544 | /* End XCConfigurationList section */
545 | };
546 | rootObject = EA037E4A1A834B9300F47B17 /* Project object */;
547 | }
548 |
--------------------------------------------------------------------------------
/Sukhasana/View/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
662 |
663 |
664 |
--------------------------------------------------------------------------------