├── Assets ├── PSD │ ├── nova_logo.psd │ ├── nova_github_header.psd │ └── nova_logo_transparent.psd └── PNG │ ├── nova_screenshot.png │ ├── nova_github_header.png │ ├── nova_logo_transparent.png │ └── nova_logo_transparent_dark.png ├── Nova ├── support │ ├── pdf │ │ ├── license.pdf │ │ └── acknowledgements.pdf │ ├── Schedulers.swift │ ├── EventMonitor.swift │ ├── Constants.swift │ ├── Extensions.swift │ ├── Prefs.swift │ └── PriceFormatter.swift ├── Assets.xcassets │ ├── Contents.json │ ├── ic_eth.imageset │ │ ├── ic_eth.png │ │ └── Contents.json │ ├── ic_neo.imageset │ │ ├── ic_neo.png │ │ └── Contents.json │ ├── ic_mail.imageset │ │ ├── ic_mail.png │ │ └── Contents.json │ ├── ic_menu.imageset │ │ ├── ic_menu.png │ │ └── Contents.json │ ├── ic_github.imageset │ │ ├── ic_github.png │ │ └── Contents.json │ ├── ic_phone.imageset │ │ ├── ic_phone.png │ │ └── Contents.json │ ├── ic_search.imageset │ │ ├── ic_search.png │ │ └── Contents.json │ ├── nova_logo.imageset │ │ ├── nova_logo.png │ │ └── Contents.json │ ├── ic_bitcoin.imageset │ │ ├── ic_bitcoin.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── nov_logo_512-1.png │ │ ├── nov_logo_512.png │ │ ├── nova_logo_1024.png │ │ ├── nova_logo_128.png │ │ ├── nova_logo_16.png │ │ ├── nova_logo_256.png │ │ ├── nova_logo_32-1.png │ │ ├── nova_logo_32.png │ │ ├── nova_logo_64.png │ │ ├── nova_logo_256-1.png │ │ └── Contents.json │ ├── ic_ethereum.imageset │ │ ├── ic_ethereum.png │ │ └── Contents.json │ ├── ic_linkedin.imageset │ │ ├── ic_linkedin.png │ │ └── Contents.json │ ├── ic_pin.imageset │ │ ├── icons8-Push Pin.png │ │ └── Contents.json │ ├── ic_settings.imageset │ │ ├── icons8-Settings-50.png │ │ └── Contents.json │ ├── ic_restart.imageset │ │ ├── icons8-Restart Filled.png │ │ └── Contents.json │ ├── ic_checkmark.imageset │ │ ├── icons8-Checkmark Filled.png │ │ └── Contents.json │ ├── ic_pin_filled.imageset │ │ ├── icons8-Push Pin Filled.png │ │ └── Contents.json │ ├── ic_checkmark_light.imageset │ │ ├── icons8-Checkmark Filled.png │ │ └── Contents.json │ ├── nova_logo_transparent.imageset │ │ ├── nova_logo_transparent.png │ │ └── Contents.json │ └── nova_logo_transparent_dark.imageset │ │ ├── nova_logo_transparent_dark.png │ │ └── Contents.json ├── Nova.entitlements ├── view │ ├── tickerlist │ │ ├── TickerListCellView.swift │ │ ├── TickerListViewModel.swift │ │ └── TickerListViewController.swift │ ├── DonateViewController.swift │ ├── ContactViewController.swift │ └── menubar │ │ ├── MenuBarViewModel.swift │ │ └── MenuBarView.swift ├── Info.plist ├── data │ ├── firebase │ │ ├── FirebaseProvider.swift │ │ └── News.swift │ ├── cryptocompare │ │ └── CryptoCompareProvider.swift │ └── coinmarketcap │ │ ├── CoinMarketCapProvider.swift │ │ └── Ticker.swift ├── AppDelegate.swift ├── repository │ ├── RemoteDataSource.swift │ ├── LocalDataSource.swift │ └── DataRepository.swift └── Injector.swift ├── Nova.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── Nova.xcworkspace └── contents.xcworkspacedata ├── Podfile ├── NovaTests ├── Info.plist └── NovaTests.swift ├── README.md ├── .gitignore ├── Podfile.lock └── LICENSE /Assets/PSD/nova_logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Assets/PSD/nova_logo.psd -------------------------------------------------------------------------------- /Nova/support/pdf/license.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/support/pdf/license.pdf -------------------------------------------------------------------------------- /Assets/PNG/nova_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Assets/PNG/nova_screenshot.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Assets/PNG/nova_github_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Assets/PNG/nova_github_header.png -------------------------------------------------------------------------------- /Assets/PSD/nova_github_header.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Assets/PSD/nova_github_header.psd -------------------------------------------------------------------------------- /Assets/PNG/nova_logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Assets/PNG/nova_logo_transparent.png -------------------------------------------------------------------------------- /Assets/PSD/nova_logo_transparent.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Assets/PSD/nova_logo_transparent.psd -------------------------------------------------------------------------------- /Nova/support/pdf/acknowledgements.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/support/pdf/acknowledgements.pdf -------------------------------------------------------------------------------- /Assets/PNG/nova_logo_transparent_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Assets/PNG/nova_logo_transparent_dark.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_eth.imageset/ic_eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_eth.imageset/ic_eth.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_neo.imageset/ic_neo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_neo.imageset/ic_neo.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_mail.imageset/ic_mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_mail.imageset/ic_mail.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_menu.imageset/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_menu.imageset/ic_menu.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_github.imageset/ic_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_github.imageset/ic_github.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_phone.imageset/ic_phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_phone.imageset/ic_phone.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_search.imageset/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_search.imageset/ic_search.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/nova_logo.imageset/nova_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/nova_logo.imageset/nova_logo.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_bitcoin.imageset/ic_bitcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_bitcoin.imageset/ic_bitcoin.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nov_logo_512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nov_logo_512-1.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nov_logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nov_logo_512.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_1024.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_128.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_16.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_256.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_32-1.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_32.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_64.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_ethereum.imageset/ic_ethereum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_ethereum.imageset/ic_ethereum.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_linkedin.imageset/ic_linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_linkedin.imageset/ic_linkedin.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_pin.imageset/icons8-Push Pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_pin.imageset/icons8-Push Pin.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/AppIcon.appiconset/nova_logo_256-1.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_settings.imageset/icons8-Settings-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_settings.imageset/icons8-Settings-50.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_restart.imageset/icons8-Restart Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_restart.imageset/icons8-Restart Filled.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_checkmark.imageset/icons8-Checkmark Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_checkmark.imageset/icons8-Checkmark Filled.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_pin_filled.imageset/icons8-Push Pin Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_pin_filled.imageset/icons8-Push Pin Filled.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_checkmark_light.imageset/icons8-Checkmark Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/ic_checkmark_light.imageset/icons8-Checkmark Filled.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/nova_logo_transparent.imageset/nova_logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/nova_logo_transparent.imageset/nova_logo_transparent.png -------------------------------------------------------------------------------- /Nova/Assets.xcassets/nova_logo_transparent_dark.imageset/nova_logo_transparent_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejJurkin/nova-osx/HEAD/Nova/Assets.xcassets/nova_logo_transparent_dark.imageset/nova_logo_transparent_dark.png -------------------------------------------------------------------------------- /Nova.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Nova.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Nova/Nova.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_eth.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_eth.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_mail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_mail.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_menu.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_menu.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_neo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_neo.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_phone.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_phone.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_bitcoin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_bitcoin.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_github.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_github.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_pin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icons8-Push Pin.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_search.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/nova_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "nova_logo.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_ethereum.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_ethereum.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_linkedin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ic_linkedin.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_restart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icons8-Restart Filled.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icons8-Settings-50.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_checkmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icons8-Checkmark Filled.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_pin_filled.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icons8-Push Pin Filled.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/ic_checkmark_light.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icons8-Checkmark Filled.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/nova_logo_transparent.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "nova_logo_transparent.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Nova/Assets.xcassets/nova_logo_transparent_dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "nova_logo_transparent_dark.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :osx, '10.11' 3 | 4 | target 'Nova' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | pod 'Alamofire' 9 | pod 'RxSwift' 10 | pod 'RxCocoa' 11 | pod 'RxAlamofire' 12 | pod 'Moya/RxSwift' 13 | pod 'RxOptional' 14 | pod 'Kingfisher', '~> 3.13.1' 15 | pod 'RealmSwift' 16 | pod 'RxRealm' 17 | pod 'Swinject' 18 | pod 'SwinjectAutoregistration' 19 | pod 'ObjectMapper' 20 | pod 'Moya-ObjectMapper', '~> 2.3.2' 21 | pod 'Moya-ObjectMapper/RxSwift', '~> 2.3.2' 22 | end 23 | -------------------------------------------------------------------------------- /NovaTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NovaTests/NovaTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NovaTests.swift 3 | // NovaTests 4 | // 5 | // Created by Andrej Jurkin on 9/2/17. 6 | // Copyright © 2017 Andrej Jurkin. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Nova 11 | 12 | class NovaTests: 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 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Nova/view/tickerlist/TickerListCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // TickerListCellView.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | 22 | import Foundation 23 | import Cocoa 24 | 25 | class TickerListCellView: NSTableCellView { 26 | 27 | @IBOutlet weak var currencyImageView: NSImageView! 28 | 29 | @IBOutlet weak var currencyName: NSTextField! 30 | 31 | @IBOutlet weak var currencyPrice: NSTextField! 32 | 33 | @IBOutlet weak var pinButton: NSButton! 34 | } 35 | -------------------------------------------------------------------------------- /Nova/view/DonateViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // DonateViewController.swift 17 | // Nova 18 | // 19 | // Created by Andrej Jurkin on 26/09/17. 20 | // 21 | 22 | import Cocoa 23 | 24 | class DonateViewController: NSViewController { 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | // Do view setup here. 29 | } 30 | 31 | override func viewDidAppear() { 32 | super.viewDidAppear() 33 | self.setTransparentTitle() 34 | AppDelegate.shared().menuBarView?.hidePopover() 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![NOVA OS X crypto currency ticker menu bar app](/Assets/PNG/nova_github_header.png) 2 | 3 | [DOWNLOAD FROM APP STORE](https://itunes.apple.com/us/app/nova-menu-bar-ticker/id1290181512) 4 | 5 | Nova is a tiny menu bar widget that displays fresh crypto-currency prices (tickers). Simply select coins you are interested in and watch the prices change in your menu bar! 6 | 7 | Nova supports all major coins as well as over 1000 altcoins. 8 | 9 | Update interval is 15 seconds, it will be possible to configure this interval in the future. 10 | 11 | If you have any feedback or suggestions, please do not hesitate to contact me at andrej.jurkin@gmail.com. 12 | 13 | Data for the app is provided by [CryptoCompare](https://www.cryptocompare.com/api/) and [CoinMarketCap](https://coinmarketcap.com/api/), thanks! 14 | 15 | Enjoy! 16 | 17 | ![NOVA OS X crypto currency ticker menu bar app](/Assets/PNG/nova_screenshot.png) 18 | 19 | ## Donate 20 | 21 | Nova is free and always will be! Your donation directly supports the development of this and other great tools. 22 | 23 | ##### BTC 24 | 1KXnhHv7JMgqzrefcbDhYXqR1cSfGJhR6x 25 | 26 | ##### ETH, ERC20 27 | 0xA88A216A1F9a1900721413314775f982816C040c 28 | 29 | ##### NEO 30 | APdpeDMqXHCx1q4x2N8t7xRvnUABjxQe6o 31 | -------------------------------------------------------------------------------- /Nova/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 0.1.6 21 | CFBundleVersion 22 | 11 23 | LSApplicationCategoryType 24 | public.app-category.finance 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2017 Andrej Jurkin. All rights reserved. 31 | NSMainStoryboardFile 32 | Main 33 | NSPrincipalClass 34 | NSApplication 35 | 36 | 37 | -------------------------------------------------------------------------------- /Nova/support/Schedulers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // Schedulers.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/22/17. 21 | // 22 | 23 | 24 | import Foundation 25 | import RxSwift 26 | 27 | class Schedulers { 28 | 29 | static var main: MainScheduler { 30 | return MainScheduler.instance 31 | } 32 | 33 | static var backgroundInteractive: ConcurrentDispatchQueueScheduler { 34 | return ConcurrentDispatchQueueScheduler(qos: .userInteractive) 35 | } 36 | 37 | static var background: ConcurrentDispatchQueueScheduler { 38 | return ConcurrentDispatchQueueScheduler(qos: .background) 39 | } 40 | 41 | static var backgroundDefault: ConcurrentDispatchQueueScheduler { 42 | return ConcurrentDispatchQueueScheduler(qos: .default) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Nova/support/EventMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // EventMonitor.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/23/17. 21 | // 22 | 23 | import Foundation 24 | import Cocoa 25 | 26 | public class EventMonitor { 27 | 28 | private var monitor: Any? 29 | 30 | private let mask: NSEventMask 31 | 32 | private let handler: (NSEvent) -> () 33 | 34 | public init(mask: NSEventMask, handler: @escaping (NSEvent) -> ()) { 35 | self.mask = mask 36 | self.handler = handler 37 | } 38 | 39 | deinit { 40 | self.stop() 41 | } 42 | 43 | func start() { 44 | self.monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) 45 | } 46 | 47 | func stop() { 48 | if let monitor = self.monitor { 49 | NSEvent.removeMonitor(monitor) 50 | self.monitor = nil 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | 16 | ## Other 17 | *.moved-aside 18 | *.xccheckout 19 | *.xcscmblueprint 20 | 21 | ## Obj-C/Swift specific 22 | *.hmap 23 | *.ipa 24 | *.dSYM.zip 25 | *.dSYM 26 | 27 | ## Playgrounds 28 | timeline.xctimeline 29 | playground.xcworkspace 30 | 31 | # Swift Package Manager 32 | # 33 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 34 | # Packages/ 35 | # Package.pins 36 | # Package.resolved 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | Pods/ 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Carthage/Build 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 57 | # screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 60 | 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output -------------------------------------------------------------------------------- /Nova/data/firebase/FirebaseProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // FirebaseProvider.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 12/29/17. 21 | // 22 | 23 | import Foundation 24 | import Moya 25 | 26 | enum FirebaseProvider { 27 | 28 | case news 29 | } 30 | 31 | extension FirebaseProvider: TargetType { 32 | var baseURL: URL { 33 | return C.firebaseBaseUrl 34 | } 35 | 36 | var path: String { 37 | switch self { 38 | case .news: 39 | return "/news.json" 40 | } 41 | } 42 | 43 | var method: Moya.Method { 44 | return .get 45 | } 46 | 47 | var parameters: [String : Any]? { 48 | return nil 49 | } 50 | 51 | var parameterEncoding: ParameterEncoding { 52 | return URLEncoding.default 53 | } 54 | 55 | var task: Task { 56 | return .request 57 | } 58 | 59 | var sampleData: Data { 60 | return Data() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Nova/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "nova_logo_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "nova_logo_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "nova_logo_32-1.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "nova_logo_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "nova_logo_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "nova_logo_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "nova_logo_256-1.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "nov_logo_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "nov_logo_512-1.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "nova_logo_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Nova/view/ContactViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // ContactViewController.swift 17 | // Nova 18 | // 19 | // Created by Andrej Jurkin on 26/09/17. 20 | // 21 | 22 | import Cocoa 23 | 24 | class ContactViewController: NSViewController { 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | // Do view setup here. 29 | 30 | } 31 | 32 | override func viewDidAppear() { 33 | super.viewDidAppear() 34 | self.setTransparentTitle() 35 | AppDelegate.shared().menuBarView?.hidePopover() 36 | } 37 | 38 | @IBAction func onLinkedinClick(_ sender: Any) { 39 | if let url = URL(string: "https://www.linkedin.com/in/andrej-jurkin-9691379a/") { 40 | NSWorkspace.shared().open(url) 41 | } 42 | } 43 | 44 | @IBAction func onGithubClick(_ sender: Any) { 45 | if let url = URL(string: "https://github.com/andrejjurkin") { 46 | NSWorkspace.shared().open(url) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Nova/support/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // Constants.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | import Foundation 23 | import Cocoa 24 | 25 | /// Project constants 26 | struct C { 27 | 28 | static let coinMarketCapBaseUrl = URL(string: "https://api.coinmarketcap.com/v1/")! 29 | 30 | static let firebaseBaseUrl = URL(string: "https://nova-48c48.firebaseio.com/")! 31 | } 32 | 33 | /// Resource constants 34 | struct R { 35 | 36 | struct String { 37 | static let appName = "N O V A" 38 | } 39 | 40 | struct Color { 41 | static let primary = NSColor(rgb: 0x193C56) 42 | static let primaryLight = NSColor(rgb: 0xEEEEEB) 43 | static let placeholderLight = NSColor(rgb: 0xCCCCCC) 44 | } 45 | 46 | struct Id { 47 | 48 | } 49 | 50 | struct Font { 51 | 52 | static let menuBarTitleAttributes = [NSFontAttributeName: NSFont(name: "Avenir", size: 12.0)!] 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Nova/data/firebase/News.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // News.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 12/30/17. 21 | // 22 | 23 | import Foundation 24 | import RealmSwift 25 | import ObjectMapper 26 | 27 | class News: RealmObject, Mappable { 28 | 29 | dynamic var id: Int = 0 30 | dynamic var title: String = "" 31 | dynamic var subtitle: String = "" 32 | dynamic var link: String = "" 33 | dynamic var imageUrl: String = "" 34 | dynamic var buttonTitle: String = "" 35 | dynamic var hidden: Bool = false 36 | 37 | required convenience init?(map: Map) { 38 | self.init() 39 | } 40 | 41 | func mapping(map: Map) { 42 | self.id <- map["id"] 43 | self.title <- map["title"] 44 | self.subtitle <- map["subtitle"] 45 | self.link <- map["link"] 46 | self.imageUrl <- map["imageUrl"] 47 | self.buttonTitle <- map["buttonTitle"] 48 | self.hidden <- map["hidden"] 49 | } 50 | 51 | override static func primaryKey() -> String? { 52 | return "id" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Nova/data/cryptocompare/CryptoCompareProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // CryptoCompareProvider.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/22/17. 21 | // 22 | 23 | import Foundation 24 | import Moya 25 | 26 | enum CryptoCompareProvider { 27 | 28 | case priceMulti(fromSymbols: [String], toSymbols: [String]) 29 | } 30 | 31 | extension CryptoCompareProvider: TargetType { 32 | 33 | var baseURL: URL { 34 | return URL(string: "https://min-api.cryptocompare.com/")! 35 | } 36 | 37 | var path: String { 38 | switch self { 39 | case .priceMulti: 40 | return "data/pricemulti" 41 | } 42 | } 43 | 44 | var method: Moya.Method { 45 | switch self { 46 | default: 47 | return .get 48 | } 49 | } 50 | 51 | var parameters: [String : Any]? { 52 | switch self { 53 | case .priceMulti(let fromSymbols, let toSymbols): 54 | 55 | return [ 56 | "fsyms": toArrayParam(data: fromSymbols), 57 | "tsyms": toArrayParam(data: toSymbols) 58 | ] 59 | } 60 | } 61 | 62 | var parameterEncoding: ParameterEncoding { 63 | return URLEncoding.queryString 64 | } 65 | 66 | var sampleData: Data { 67 | return Data() 68 | } 69 | 70 | var task: Task { 71 | return .request 72 | } 73 | 74 | /// Workaround for CryptoCompare api, it does not accept a standard notation 75 | func toArrayParam(data: [String]) -> String { 76 | return data.joined(separator: ",") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Nova/data/coinmarketcap/CoinMarketCapProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // CoinMarketCapProvider.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | 23 | import Foundation 24 | import Moya 25 | 26 | enum CoinMarketCapProvider { 27 | 28 | case allTickers 29 | 30 | case topTickers(limit: Int) 31 | 32 | case ticker(currencyName: String) 33 | } 34 | 35 | extension CoinMarketCapProvider: TargetType { 36 | 37 | var baseURL: URL { 38 | return C.coinMarketCapBaseUrl 39 | } 40 | 41 | var path: String { 42 | switch self { 43 | case .allTickers: 44 | return "ticker" 45 | case .topTickers: 46 | return "ticker" 47 | case .ticker(let currencyName): 48 | return "ticker/\(currencyName)" 49 | } 50 | } 51 | 52 | var method: Moya.Method { 53 | switch self { 54 | default: 55 | return .get 56 | } 57 | } 58 | 59 | var parameters: [String : Any]? { 60 | switch self { 61 | case .allTickers: 62 | //TODO: pass target currency as a parameter 63 | return ["limit": 0, "convert": Prefs.shared.targetCurrency] 64 | case .topTickers(let limit): 65 | return ["limit": limit, "convert": Prefs.shared.targetCurrency] 66 | default: 67 | return nil 68 | } 69 | } 70 | 71 | var parameterEncoding: ParameterEncoding { 72 | return URLEncoding.default 73 | } 74 | 75 | var sampleData: Data { 76 | return Data() 77 | } 78 | 79 | var task: Task { 80 | return .request 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Nova/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // AppDelegate.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | import Cocoa 23 | import RxSwift 24 | import RealmSwift 25 | 26 | @NSApplicationMain 27 | class AppDelegate: NSObject, NSApplicationDelegate { 28 | 29 | var menuBarView: MenuBarView? 30 | 31 | static func shared() -> AppDelegate { 32 | return NSApp.delegate as! AppDelegate 33 | } 34 | 35 | func applicationDidFinishLaunching(_ aNotification: Notification) { 36 | self.initRealm() 37 | self.menuBarView = MenuBarView() 38 | self.fileNotifications() 39 | } 40 | 41 | func initRealm() { 42 | let realmConfig = Realm.Configuration(deleteRealmIfMigrationNeeded: true) 43 | Realm.Configuration.defaultConfiguration = realmConfig 44 | } 45 | 46 | func clearRealm() { 47 | let realm = try! Realm() 48 | 49 | try! realm.write { 50 | realm.deleteAll() 51 | } 52 | } 53 | 54 | func onWakeNote(note: NSNotification) { 55 | self.menuBarView?.resume() 56 | } 57 | 58 | func onSleepNote(note: NSNotification) { 59 | self.menuBarView?.pause() 60 | } 61 | 62 | func fileNotifications() { 63 | NSWorkspace.shared().notificationCenter.addObserver( 64 | self, selector: #selector(onWakeNote(note:)), 65 | name: Notification.Name.NSWorkspaceDidWake, object: nil) 66 | 67 | NSWorkspace.shared().notificationCenter.addObserver( 68 | self, selector: #selector(onSleepNote(note:)), 69 | name: Notification.Name.NSWorkspaceWillSleep, object: nil) 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Nova/data/coinmarketcap/Ticker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // Ticker.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | 23 | import Foundation 24 | import RealmSwift 25 | import ObjectMapper 26 | 27 | class Ticker: RealmObject, Mappable { 28 | 29 | // Realm primary key 30 | dynamic var symbol: String = "" 31 | dynamic var id: String = "" 32 | dynamic var name: String = "" 33 | dynamic var isPinned = false 34 | dynamic var price: Double = 0 35 | dynamic var priceBtc: Double = 0 36 | dynamic var rank: Int = 0 37 | 38 | required convenience init?(map: Map) { 39 | self.init() 40 | } 41 | 42 | func mapping(map: Map) { 43 | self.symbol <- map["symbol"] 44 | self.id <- map["id"] 45 | self.name <- map["name"] 46 | 47 | var priceUsdStr: String = "" 48 | var priceBtcStr: String = "" 49 | var rankStr: String = "" 50 | 51 | let targetCurrency = Prefs.shared.targetCurrency.lowercased() 52 | 53 | priceUsdStr <- map["price_\(targetCurrency)"] 54 | priceBtcStr <- map["price_btc"] 55 | rankStr <- map["rank"] 56 | 57 | self.price = Double(priceUsdStr) ?? 0 58 | self.priceBtc = Double(priceBtcStr) ?? 0 59 | self.rank = Int(rankStr) ?? 0 60 | } 61 | 62 | override static func primaryKey() -> String? { 63 | return "symbol" 64 | } 65 | 66 | /// Update Realm with dictionary to prevent from overwriting ignored properties 67 | func toDictionary() -> [String: Any] { 68 | return [ 69 | "id": id, 70 | "symbol": symbol, 71 | "name": name, 72 | "price": price, 73 | "priceBtc": priceBtc, 74 | "rank": rank, 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.5.1) 3 | - Kingfisher (3.13.1) 4 | - Moya (8.0.5): 5 | - Moya/Core (= 8.0.5) 6 | - Moya-ObjectMapper (2.3.2): 7 | - Moya-ObjectMapper/Core (= 2.3.2) 8 | - Moya-ObjectMapper/Core (2.3.2): 9 | - Moya (~> 8.0) 10 | - ObjectMapper (~> 2.2) 11 | - Moya-ObjectMapper/RxSwift (2.3.2): 12 | - Moya-ObjectMapper/Core 13 | - Moya/RxSwift 14 | - RxSwift (~> 3.1) 15 | - Moya/Core (8.0.5): 16 | - Alamofire (~> 4.1) 17 | - Result (~> 3.0) 18 | - Moya/RxSwift (8.0.5): 19 | - Moya/Core 20 | - RxSwift (~> 3.0) 21 | - ObjectMapper (2.2.9) 22 | - Realm (2.10.2): 23 | - Realm/Headers (= 2.10.2) 24 | - Realm/Headers (2.10.2) 25 | - RealmSwift (2.10.2): 26 | - Realm (= 2.10.2) 27 | - Result (3.2.4) 28 | - RxAlamofire (3.0.3): 29 | - RxAlamofire/Core (= 3.0.3) 30 | - RxAlamofire/Core (3.0.3): 31 | - Alamofire (~> 4.0) 32 | - RxSwift (~> 3.0) 33 | - RxCocoa (3.6.1): 34 | - RxSwift (~> 3.6) 35 | - RxOptional (3.2.0): 36 | - RxCocoa 37 | - RxSwift 38 | - RxRealm (0.7.2): 39 | - RealmSwift (~> 2) 40 | - RxSwift (~> 3) 41 | - RxSwift (3.6.1) 42 | - Swinject (2.1.1) 43 | - SwinjectAutoregistration (2.1.0): 44 | - Swinject (~> 2.1) 45 | 46 | DEPENDENCIES: 47 | - Alamofire 48 | - Kingfisher (~> 3.13.1) 49 | - Moya-ObjectMapper (~> 2.3.2) 50 | - Moya-ObjectMapper/RxSwift (~> 2.3.2) 51 | - Moya/RxSwift 52 | - ObjectMapper 53 | - RealmSwift 54 | - RxAlamofire 55 | - RxCocoa 56 | - RxOptional 57 | - RxRealm 58 | - RxSwift 59 | - Swinject 60 | - SwinjectAutoregistration 61 | 62 | SPEC CHECKSUMS: 63 | Alamofire: 2d95912bf4c34f164fdfc335872e8c312acaea4a 64 | Kingfisher: ed9707a409cc47fddafbcd12a43a9757e3600af5 65 | Moya: c37eec09a098ba9991b5a963b291fc5704bdb9ef 66 | Moya-ObjectMapper: 1b05f10bb69fb6c8750d497d2974c1c63c08c3a9 67 | ObjectMapper: 63cfe41bc6f8e7c8f44344c49901b8ae7de14c52 68 | Realm: 0ef72b837fb67e9f4b098bac771ddd72c7fdbb69 69 | RealmSwift: 07a9ae0505091eda6b2ee7c190c3786d6e90a7b0 70 | Result: d2d07204ce72856f1fd9130bbe42c35a7b0fea10 71 | RxAlamofire: 5eb39188c4917ad98127c0ecb4878a61b7517003 72 | RxCocoa: 84a08739ab186248c7f31ce4ee92d6f8a947d690 73 | RxOptional: d2d6a68064bc16b4801332c40663967f481b0234 74 | RxRealm: 9cf5c15c5312fc770aaa542a360cffec4916e63a 75 | RxSwift: f9de85ea20cd2f7716ee5409fc13523dc638e4e4 76 | Swinject: afea49fd95f392b41171669f165842879c48fad6 77 | SwinjectAutoregistration: 7d8ac2c23718c78286adf7590c6d0671b4ccc834 78 | 79 | PODFILE CHECKSUM: 82faa986bafd3f70ed7abfff83bb9623e3b541da 80 | 81 | COCOAPODS: 1.3.1 82 | -------------------------------------------------------------------------------- /Nova/view/menubar/MenuBarViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // MenuBarViewModel.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | 22 | import Foundation 23 | import RxSwift 24 | 25 | class MenuBarViewModel { 26 | 27 | let repo: DataRepository 28 | 29 | let prefs: Prefs 30 | 31 | var menuBarText = Variable(R.String.appName) 32 | 33 | let disposeBag = DisposeBag() 34 | 35 | init(repo: DataRepository, prefs: Prefs) { 36 | self.repo = repo 37 | self.prefs = prefs 38 | } 39 | 40 | func subscribe() { 41 | // Watch when list with pinned tickers changes 42 | self.repo.getPinnedTickers() 43 | .retry(5) 44 | .flatMap({ tickers -> Observable<[String]> in 45 | 46 | // If we don't have any pinned tickers, show app name 47 | guard tickers.count > 0 else { 48 | self.repo.disposeTickerSubscription() 49 | self.menuBarText.value = R.String.appName 50 | return Observable.just([]) 51 | } 52 | 53 | var menuBarText = "" 54 | var tickerSymbols: [String] = [] 55 | 56 | let priceFormatter = PriceFormatter(displayCurrency: self.prefs.displayCurrency, decimalFormat: self.prefs.menuBarFormat) 57 | 58 | // Create menu bar string representation 59 | for ticker in tickers { 60 | menuBarText.append(priceFormatter.formatWithTickerSymbol(ticker: ticker)) 61 | tickerSymbols.append(ticker.symbol) 62 | } 63 | 64 | self.menuBarText.value = menuBarText.trim() 65 | return Observable.just(tickerSymbols) 66 | }) 67 | 68 | // Distinct if pinned tickers array has the same size 69 | // This is to avoid re-subscribing after values update 70 | .distinctUntilChanged({ (old, new) -> Bool in 71 | return old.count == new.count 72 | }) 73 | .filter { $0.count != 0 } 74 | .subscribe(onNext: { tickerSymbols in 75 | 76 | // Subscribe for updates for given ticker symbols 77 | self.repo.subscribeForTickerUpdates(baseSymbols: tickerSymbols) 78 | }) 79 | .addDisposableTo(disposeBag) 80 | 81 | self.repo.subscribeForGlobalUpdates() 82 | } 83 | 84 | func unsubscribe() { 85 | self.repo.disposeRefreshSubscriptions() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Nova/support/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // Extensions.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | 23 | import Foundation 24 | import Cocoa 25 | import RealmSwift 26 | import RxSwift 27 | 28 | typealias RealmObject = Object 29 | 30 | extension NSViewController { 31 | func setTransparentTitle() { 32 | self.view.window?.titleVisibility = .hidden 33 | self.view.window?.titlebarAppearsTransparent = true 34 | self.view.window?.styleMask.insert(.fullSizeContentView) 35 | self.view.window?.isOpaque = false 36 | self.view.window?.backgroundColor = NSColor.clear 37 | } 38 | } 39 | 40 | extension NSColor { 41 | convenience init(red: Int, green: Int, blue: Int) { 42 | assert(red >= 0 && red <= 255, "Invalid red component") 43 | assert(green >= 0 && green <= 255, "Invalid green component") 44 | assert(blue >= 0 && blue <= 255, "Invalid blue component") 45 | 46 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) 47 | } 48 | 49 | convenience init(rgb: Int) { 50 | self.init( 51 | red: (rgb >> 16) & 0xFF, 52 | green: (rgb >> 8) & 0xFF, 53 | blue: rgb & 0xFF 54 | ) 55 | } 56 | } 57 | 58 | extension NSTextField { 59 | 60 | var cursorColor: NSColor? { 61 | get { 62 | if let fieldEditor = self.window?.fieldEditor(true, for: self) as? NSTextView { 63 | return fieldEditor.insertionPointColor 64 | } 65 | return nil 66 | } 67 | set { 68 | if let fieldEditor = self.window?.fieldEditor(true, for: self) as? NSTextView, 69 | let cursorColor = newValue { 70 | fieldEditor.insertionPointColor = cursorColor 71 | } 72 | } 73 | } 74 | } 75 | 76 | extension String { 77 | func trim() -> String { 78 | return self.trimmingCharacters(in: NSCharacterSet.whitespaces) 79 | } 80 | } 81 | 82 | extension ObservableType { 83 | public func retryWithDelay(timeInterval: Int) -> Observable { 84 | return retryWhen { (attempts: Observable) -> Observable in 85 | return Observable 86 | .zip(attempts, Observable.just(timeInterval), resultSelector: { (o1, o2) in 87 | return o2 88 | }) 89 | .flatMap { i -> Observable in 90 | return Observable.timer(RxTimeInterval(i), period: RxTimeInterval(i), scheduler: MainScheduler.instance) 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Nova/view/menubar/MenuBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // MenuBarView.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | 22 | import Foundation 23 | import Cocoa 24 | import RxSwift 25 | import RxCocoa 26 | 27 | /// Menu bar view displays selected tickers in menu bar 28 | class MenuBarView: NSObject { 29 | 30 | /// Status item used to display coin tickers 31 | let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength) 32 | 33 | /// Content wrapper 34 | let popover = NSPopover() 35 | 36 | let viewModel = Injector.inject(type: MenuBarViewModel.self) 37 | 38 | let disposeBag = DisposeBag() 39 | 40 | private var eventMonitor: EventMonitor? 41 | 42 | override init() { 43 | super.init() 44 | 45 | self.statusItem.button?.action = #selector(togglePopover) 46 | self.statusItem.button?.target = self 47 | 48 | self.bindUi() 49 | 50 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 51 | 52 | let popoverViewController = storyboard.instantiateController( 53 | withIdentifier: "popover") as! TickerListViewController 54 | 55 | self.popover.contentViewController = popoverViewController 56 | 57 | // Close popover on any click outside of the app 58 | self.eventMonitor = EventMonitor( 59 | mask: [.leftMouseDown, .rightMouseDown], 60 | handler: { [weak self] event in 61 | self?.hidePopover() 62 | }) 63 | } 64 | 65 | func togglePopover() { 66 | if popover.isShown { 67 | self.hidePopover() 68 | } else { 69 | self.showPopover() 70 | } 71 | } 72 | 73 | func showPopover() { 74 | if let button = statusItem.button { 75 | self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) 76 | self.eventMonitor?.start() 77 | } 78 | } 79 | 80 | func hidePopover() { 81 | self.popover.performClose(nil) 82 | self.eventMonitor?.stop() 83 | } 84 | 85 | func setStatusItemTitle(title: String) { 86 | let title = NSAttributedString(string: title, attributes: R.Font.menuBarTitleAttributes) 87 | 88 | self.statusItem.attributedTitle = title 89 | } 90 | 91 | func refresh() { 92 | self.viewModel.subscribe() 93 | } 94 | 95 | func pause() { 96 | self.viewModel.unsubscribe() 97 | } 98 | 99 | func resume() { 100 | self.viewModel.subscribe() 101 | } 102 | 103 | private func bindUi() { 104 | self.viewModel.subscribe() 105 | self.viewModel.menuBarText.asObservable().subscribe(onNext: { text in 106 | self.setStatusItemTitle(title: text) 107 | }) 108 | .addDisposableTo(disposeBag) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Nova/repository/RemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // RemoteDataSource.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | 23 | import Foundation 24 | import RxSwift 25 | import Moya 26 | import ObjectMapper 27 | import Moya_ObjectMapper 28 | 29 | class RemoteDataSource { 30 | 31 | private var coinMarketCapProvider: RxMoyaProvider 32 | 33 | private var cryptoCompareProvider: RxMoyaProvider 34 | 35 | private var firebaseProvider: RxMoyaProvider 36 | 37 | private let providerPlugins = [NetworkLoggerPlugin()] 38 | 39 | init(coinMarketCapProvider: RxMoyaProvider, 40 | cryptoCompareProvider: RxMoyaProvider, 41 | firebaseProvider: RxMoyaProvider) { 42 | 43 | self.coinMarketCapProvider = coinMarketCapProvider 44 | self.cryptoCompareProvider = cryptoCompareProvider 45 | self.firebaseProvider = firebaseProvider 46 | } 47 | 48 | /// Get all available tickers from CoinMarketCap 49 | func getAllTickers() -> Observable<[Ticker]> { 50 | return self.coinMarketCapProvider.request(.allTickers) 51 | .observeOn(Schedulers.backgroundInteractive) 52 | .mapArray(Ticker.self) 53 | 54 | } 55 | 56 | /// Get top N (limit) tickers, sorted by market cap 57 | func getTopTickers(limit: Int) -> Observable<[Ticker]> { 58 | return self.coinMarketCapProvider.request(.topTickers(limit: limit)) 59 | .mapArray(Ticker.self) 60 | } 61 | 62 | /// Get ticker for a single crypto currency 63 | func getTicker(currencyName: String) -> Observable { 64 | return self.coinMarketCapProvider.request(.ticker(currencyName: currencyName)) 65 | .mapObject(Ticker.self) 66 | } 67 | 68 | /// Get tickers from CryptoCompare api 69 | /// 70 | /// - parameters: 71 | /// - base: The array of ticker symbols (BTC, ETH, XRP...) 72 | /// - target: The optional array of fiat target symbols we are converting into (USD, EUR...) 73 | /// Defaults to [\"USD\"] 74 | func getTickers(base: [String], target: [String] = ["USD"]) -> Observable<[String: [String: Double]]> { 75 | return self.cryptoCompareProvider 76 | .request(.priceMulti(fromSymbols: base, toSymbols: target)) 77 | .filterSuccessfulStatusCodes() 78 | .map { response in 79 | return try JSONSerialization.jsonObject( 80 | with: response.data, options: []) as! [String: [String: Double]] 81 | } 82 | } 83 | 84 | // Get news from Firebase 85 | func getNews() -> Observable { 86 | return firebaseProvider.request(.news) 87 | .filterSuccessfulStatusCodes() 88 | .mapObject(News.self) 89 | .do(onNext: { news in 90 | print("Firebase news fetched successfully:\(news)") 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Nova/Injector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // Injector.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | 22 | import Foundation 23 | import Swinject 24 | import Moya 25 | import SwinjectAutoregistration 26 | 27 | /// Swinject dependency injection wrapper 28 | class Injector { 29 | 30 | private static let instance = Injector() 31 | 32 | /// App level dependencies, top level di container 33 | private let mainContainer = Container() 34 | 35 | private init() { 36 | self.registerGlobalDependencies() 37 | self.registerProviders() 38 | self.registerDataSources() 39 | self.registerViewModels() 40 | } 41 | 42 | /// Inject, resolve a dependency with the given type 43 | /// Throws exception if the given type can not be resolved 44 | public static func inject(type: T.Type) -> T { 45 | return instance.mainContainer.resolve(type)! 46 | } 47 | 48 | // MARK: Global dependencies 49 | 50 | private func registerGlobalDependencies() { 51 | 52 | self.mainContainer.register(AppDelegate.self, factory: { _ in 53 | return NSApp.delegate as! AppDelegate 54 | }) 55 | 56 | self.mainContainer.autoregister(Prefs.self, initializer: Prefs.init) 57 | } 58 | 59 | // MARK: Providers 60 | 61 | private func registerProviders() { 62 | self.mainContainer.register(RxMoyaProvider.self) { _ in 63 | return RxMoyaProvider() 64 | } 65 | 66 | self.mainContainer.register(RxMoyaProvider.self) { _ in 67 | return RxMoyaProvider() 68 | } 69 | 70 | self.mainContainer.register(RxMoyaProvider.self) { _ in 71 | return RxMoyaProvider() 72 | } 73 | } 74 | 75 | 76 | // MARK: Data sources 77 | 78 | /// All data sources should be registered in container object scope (singleton) 79 | private func registerDataSources() { 80 | 81 | self.mainContainer.autoregister( 82 | RemoteDataSource.self, initializer: RemoteDataSource.init) 83 | .inObjectScope(.container) 84 | 85 | self.mainContainer.autoregister( 86 | LocalDataSource.self, initializer: LocalDataSource.init) 87 | .inObjectScope(.container) 88 | 89 | self.mainContainer.autoregister( 90 | DataRepository.self, initializer: DataRepository.init) 91 | .inObjectScope(.container) 92 | } 93 | 94 | // MARK: View models 95 | 96 | private func registerViewModels() { 97 | 98 | self.mainContainer.autoregister( 99 | MenuBarViewModel.self, initializer: MenuBarViewModel.init) 100 | 101 | self.mainContainer.autoregister( 102 | TickerListViewModel.self, initializer: TickerListViewModel.init) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Nova/support/Prefs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // Prefs.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Class for accessing and modifying preference data 26 | /// returned by NSUserDefaults. 27 | /// 28 | /// All clients should use a shared instance of this class 29 | class Prefs { 30 | 31 | static let shared = Prefs() 32 | 33 | /// Key values for acessing and modifying user defaults data 34 | struct Key { 35 | 36 | static let refreshInterval = "refresh_interval" 37 | 38 | static let pinnedSymbols = "pinned_symbols" 39 | 40 | static let targetCurrency = "target_currency" 41 | 42 | static let displayCurrency = "display_currency" 43 | 44 | static let showCurrencySymbol = "show_currency_symbol" 45 | 46 | static let menuBarFormat = "menu_bar_format" 47 | } 48 | 49 | let userDefaults = UserDefaults.standard 50 | 51 | /// Ticker refresh interval (seconds), default 30.0 52 | var rereshInterval: Float { 53 | get { 54 | return self.userDefaults.float(forKey: Key.refreshInterval) 55 | } 56 | 57 | set { 58 | self.userDefaults.set(newValue, forKey: Key.refreshInterval) 59 | } 60 | } 61 | 62 | /// Ticker symbols pinned to be displayed in menu bar 63 | /// 64 | /// - key: The currency symbol (ETH, BTC, etc.) 65 | /// - value: The latest price value 66 | /// 67 | /// Defaults to empty dictionry 68 | var pinedCurrencies: [String: Double] { 69 | get { 70 | if let value = self.userDefaults 71 | .dictionary(forKey: Key.pinnedSymbols) as? [String: Double] { 72 | 73 | return value 74 | } 75 | return [:] 76 | } 77 | 78 | set { 79 | self.userDefaults.set(newValue, forKey: Key.pinnedSymbols) 80 | } 81 | } 82 | 83 | /// Target currency to convert all coins into, default USD 84 | var targetCurrency: String { 85 | get { 86 | if let target = self.userDefaults.string(forKey: Key.targetCurrency) { 87 | return target 88 | } 89 | return "USD" 90 | } 91 | 92 | set { 93 | self.userDefaults.set(newValue, forKey: Key.targetCurrency) 94 | } 95 | } 96 | 97 | /// The currency we use to display prices 98 | /// For example BTC and SAT should both use BTC as a target currency, 99 | /// but we also need to be able to display them differently 100 | var displayCurrency: String { 101 | get { 102 | if let displayCurrency = self.userDefaults.string(forKey: Key.displayCurrency) { 103 | return displayCurrency 104 | } 105 | return "USD" 106 | } 107 | 108 | set { 109 | self.userDefaults.set(newValue, forKey: Key.displayCurrency) 110 | } 111 | } 112 | 113 | /// Menu bar number format (decimal points or significant digits) 114 | var menuBarFormat: String { 115 | get { 116 | if let target = self.userDefaults.string(forKey: Key.menuBarFormat) { 117 | return target 118 | } 119 | return "auto" 120 | } 121 | 122 | set { 123 | self.userDefaults.set(newValue, forKey: Key.menuBarFormat) 124 | } 125 | } 126 | 127 | var showCurrencySymbol: Bool { 128 | get { 129 | return self.userDefaults.bool(forKey: Key.showCurrencySymbol) 130 | } 131 | 132 | set { 133 | self.userDefaults.set(newValue, forKey: Key.showCurrencySymbol) 134 | } 135 | } 136 | 137 | init() { 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Nova/support/PriceFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // PriceFormatter.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 10/29/17. 21 | // 22 | 23 | import Foundation 24 | 25 | class PriceFormatter { 26 | 27 | let displaySymbol: String 28 | let displayCurrency: String 29 | let decimalFormat: String 30 | 31 | let decimalNumberFormatter: NumberFormatter 32 | 33 | init(displayCurrency: String, decimalFormat: String?) { 34 | self.displayCurrency = displayCurrency 35 | self.decimalFormat = decimalFormat != nil ? decimalFormat! : "auto" 36 | 37 | self.decimalNumberFormatter = NumberFormatter() 38 | decimalNumberFormatter.numberStyle = .decimal 39 | decimalNumberFormatter.groupingSeparator = "," 40 | decimalNumberFormatter.groupingSize = 3 41 | decimalNumberFormatter.minimumFractionDigits = 0 42 | decimalNumberFormatter.maximumFractionDigits = 0 43 | 44 | let locale = NSLocale(localeIdentifier: self.displayCurrency) 45 | self.displaySymbol = locale.displayName( 46 | forKey: NSLocale.Key.currencySymbol, value: self.displayCurrency) ?? displayCurrency 47 | } 48 | 49 | /// Format with ticker symbol (PAY 1.70) 50 | func formatWithTickerSymbol(ticker: Ticker) -> String { 51 | return self.format(ticker: ticker, symbol: ticker.symbol) 52 | } 53 | 54 | /// Format with target symbol ($ 1.70) 55 | func formatWithTargetSymbol(ticker: Ticker) -> String { 56 | return self.format(ticker: ticker, symbol: self.displaySymbol) 57 | } 58 | 59 | /// Format with provided symbol (SYMBOL PRICE) 60 | func format(ticker: Ticker, symbol: String) -> String { 61 | // significant digit formatting 62 | if self.decimalFormat.prefix(1) == "s" { 63 | let valueFormatter = NumberFormatter() 64 | valueFormatter.usesSignificantDigits = true 65 | var breakPoint: Double = 1000 66 | 67 | switch self.decimalFormat { 68 | // 3 significant digits 69 | case "s3": 70 | valueFormatter.minimumSignificantDigits = 3 71 | valueFormatter.maximumSignificantDigits = 3 72 | breakPoint = 1000 73 | break 74 | // 4 significant digits 75 | case "s4": 76 | valueFormatter.minimumSignificantDigits = 4 77 | valueFormatter.maximumSignificantDigits = 4 78 | breakPoint = 10000 79 | break 80 | // 5 significant digits 81 | case "s5": 82 | valueFormatter.minimumSignificantDigits = 5 83 | valueFormatter.maximumSignificantDigits = 5 84 | breakPoint = 100000 85 | break 86 | default: 87 | break 88 | } 89 | 90 | let price = ticker.price >= breakPoint ? ticker.price / 1000 : ticker.price 91 | return "\(symbol) \(String(format: ticker.price >= breakPoint ? "%@K" : "%@", valueFormatter.string(from: NSNumber(value: price))!)) " 92 | } 93 | 94 | let format = self.getPriceFormat(ticker: ticker) 95 | 96 | if displayCurrency == "SAT" { 97 | let satPrice = ticker.price * 100000000 98 | let formattedPrice = decimalNumberFormatter.string(for: satPrice) ?? "\(satPrice)" 99 | return "\(symbol) \(formattedPrice) " 100 | } 101 | 102 | return "\(symbol) \(String(format: format, ticker.price)) " 103 | } 104 | 105 | func getPriceFormat(ticker: Ticker) -> String { 106 | switch self.decimalFormat { 107 | case "d0": 108 | return "%.0f" 109 | case "d1": 110 | return "%.1f" 111 | case "d2": 112 | return "%.2f" 113 | case "d3": 114 | return "%.3f" 115 | default: 116 | break 117 | } 118 | 119 | if self.displayCurrency == "BTC" { 120 | return "%.8f" 121 | } else if self.displayCurrency == "SAT" { 122 | return "%.0f" 123 | } 124 | 125 | return ticker.price < 1 ? "%.4f" : "%.2f" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Nova/repository/LocalDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // LocalDataSource.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | 22 | import Foundation 23 | import RealmSwift 24 | import RxRealm 25 | import RxSwift 26 | 27 | class LocalDataSource { 28 | 29 | let defaultSortOrder = [ 30 | SortDescriptor(keyPath: "isPinned", ascending: false), 31 | SortDescriptor(keyPath: "rank", ascending: true) 32 | ] 33 | 34 | var prefs: Prefs 35 | 36 | init(prefs: Prefs) { 37 | self.prefs = prefs 38 | } 39 | 40 | /// Asyncroniously cache tickers into Realm 41 | func saveTickersAsync(tickers: [Ticker]) { 42 | DispatchQueue(label: "realm").async { 43 | autoreleasepool { 44 | self.saveTickers(tickers: tickers) 45 | } 46 | } 47 | } 48 | 49 | /// Synchroniously cache tickers into Realm 50 | func saveTickers(tickers: [Ticker]) { 51 | let realm = try! Realm() 52 | try! realm.write { 53 | for ticker in tickers { 54 | // Generic fix for symbol collisions. 55 | // Some unknown altcoins may have colliding symbols with (BTC, ETH..), this is a problem, 56 | // because we use ticker symbol as a primary key for cryptocompare requests. 57 | // The trade-off is to drop altcoins with colliding symbols and only display the ticker with the highest market cap. 58 | if let localTicker = realm.object(ofType: Ticker.self, forPrimaryKey: ticker.symbol) { 59 | // Skip all colliding ticker symbols 60 | if localTicker.id != ticker.id && localTicker.rank < ticker.rank { 61 | continue 62 | } 63 | } 64 | 65 | realm.create(Ticker.self, value: ticker.toDictionary(), update: true) 66 | } 67 | } 68 | } 69 | 70 | /// Cache a single ticker into Realm 71 | func saveTicker(ticker: Ticker) { 72 | let realm = try! Realm() 73 | 74 | try! realm.write { 75 | realm.create(Ticker.self, value: ticker.toDictionary(), update: true) 76 | } 77 | } 78 | 79 | func updateTickerPrices(tickers: [String: [String: Double]]) { 80 | let realm = try! Realm() 81 | 82 | try! realm.write { 83 | for (key, prices) in tickers { 84 | 85 | if let updatedPrice = prices[prefs.targetCurrency] { 86 | realm.create(Ticker.self, value: ["symbol": key, "price": updatedPrice], update: true) 87 | } 88 | } 89 | } 90 | } 91 | 92 | func updateTickerPrice(symbol: String, newPrice: Double) { 93 | let realm = try! Realm() 94 | 95 | try! realm.write { 96 | realm.create(Ticker.self, value: ["symbol": symbol, "priceUsd": newPrice], update: true) 97 | } 98 | } 99 | 100 | /// Return all tickers cached in Realm 101 | /// - Ordered by market cap, pinned tickers first 102 | func getAllTickers() -> Observable<[Ticker]> { 103 | let realm = try! Realm() 104 | 105 | let tickers = realm 106 | .objects(Ticker.self) 107 | .sorted(by: defaultSortOrder) 108 | 109 | return Observable.array(from: tickers) 110 | } 111 | 112 | /// Get pinned tickers sorted by orderIndex 113 | func getPinnedTickers() -> Observable<[Ticker]> { 114 | let realm = try! Realm() 115 | 116 | let pinnedTickers = realm 117 | .objects(Ticker.self) 118 | .filter("isPinned = true") 119 | .sorted(by: defaultSortOrder) 120 | 121 | return Observable.array(from: pinnedTickers) 122 | } 123 | 124 | func isTickerPinned(symbol: String) -> Bool { 125 | let realm = try! Realm() 126 | 127 | guard let ticker = 128 | realm.object(ofType: Ticker.self, forPrimaryKey: symbol) else { 129 | return false 130 | } 131 | 132 | return ticker.isPinned 133 | } 134 | 135 | /// Set ticker as pinned (show in menu bar) 136 | func pinTicker(symbol: String) { 137 | let realm = try! Realm() 138 | 139 | try! realm.write { 140 | realm.create(Ticker.self, value: ["symbol": symbol, "isPinned": true], update: true) 141 | } 142 | } 143 | 144 | /// Unpin ticker (remove from menu bar) 145 | func unpinTicker(symbol: String) { 146 | let realm = try! Realm() 147 | 148 | try! realm.write { 149 | realm.create(Ticker.self, value: ["symbol": symbol, "isPinned": false], update: true) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Nova/view/tickerlist/TickerListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // TickerListViewModel.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | 23 | import Foundation 24 | import RxSwift 25 | import RxCocoa 26 | 27 | /// Model for TickerList view 28 | class TickerListViewModel { 29 | 30 | private let repo = Injector.inject(type: DataRepository.self) 31 | 32 | private let prefs = Injector.inject(type: Prefs.self) 33 | 34 | /// Raw unfiltered data 35 | private var data: [Ticker] = [] 36 | 37 | /// Raw data filtered with search string 38 | var filteredData = Variable<[Ticker]>([]) 39 | 40 | /// User input, filter raw data 41 | var searchString = Variable("") 42 | 43 | /// Number of rows for tableview 44 | var numberOfRows: Int { 45 | return filteredData.value.count 46 | } 47 | 48 | /// Callback to notify tableview of data changes 49 | var reloadDataCallback: (()->())? 50 | 51 | /// True while repository processing data in background 52 | /// Bind to UI to show indicator 53 | var isRefreshing = Variable(false) 54 | 55 | /// Firebase news object 56 | var news = Variable(News()) 57 | 58 | var priceFormatter: PriceFormatter 59 | 60 | /// Rx subscriptions 61 | let disposeBag = DisposeBag() 62 | 63 | var displayCurrency: String { 64 | set { 65 | self.priceFormatter = PriceFormatter(displayCurrency: newValue, decimalFormat: nil) 66 | if newValue == "SAT" { 67 | self.prefs.targetCurrency = "BTC" 68 | } else { 69 | self.prefs.targetCurrency = newValue 70 | } 71 | self.prefs.displayCurrency = newValue 72 | } 73 | get { 74 | return prefs.displayCurrency 75 | } 76 | } 77 | 78 | var menuBarFormat: String { 79 | set { 80 | self.prefs.menuBarFormat = newValue 81 | } 82 | get { 83 | return prefs.menuBarFormat 84 | } 85 | } 86 | 87 | /// CMC image format 88 | private let imageUrlFormat = "https://files.coinmarketcap.com/static/img/coins/128x128/%@.png" 89 | 90 | init() { 91 | self.priceFormatter = PriceFormatter(displayCurrency: self.prefs.displayCurrency, decimalFormat:nil) 92 | 93 | self.repo.getAllTickers() 94 | .subscribe(onNext: { tickers in 95 | self.data = tickers 96 | self.filteredData.value = self.data 97 | self.filterData(query: self.searchString.value) 98 | }) 99 | .addDisposableTo(disposeBag) 100 | 101 | self.searchString 102 | .asObservable() 103 | .throttle(0.3, scheduler: MainScheduler.instance) 104 | .distinctUntilChanged() 105 | .subscribe(onNext: { searchStr in 106 | self.filterData(query: searchStr) 107 | }) 108 | .addDisposableTo(disposeBag) 109 | 110 | self.repo.getNews() 111 | .subscribe(onNext: { news in 112 | self.news.value = news; 113 | }) 114 | .addDisposableTo(disposeBag) 115 | } 116 | 117 | /// Get single ticker for tableview row 118 | func getTicker(row: Int) -> Ticker { 119 | return self.filteredData.value[row] 120 | } 121 | 122 | /// Currency name for tableview row 123 | func getCurrencyName(row: Int) -> String { 124 | return self.getTicker(row: row).name 125 | } 126 | 127 | /// Image url for tableview row 128 | func getCurrencyImageUrl(row: Int) -> URL? { 129 | 130 | let imageName = getTicker(row: row).name 131 | .lowercased() 132 | .replacingOccurrences(of: " ", with: "-") 133 | 134 | let urlString = String.init(format: imageUrlFormat, imageName) 135 | return URL(string: urlString) 136 | } 137 | 138 | func getTargetPrice(row: Int) -> String { 139 | let ticker = self.getTicker(row: row) 140 | return self.priceFormatter.formatWithTargetSymbol(ticker: ticker) 141 | } 142 | 143 | func pinStatusChanged(row: Int, pinned: Bool) { 144 | let ticker = getTicker(row: row) 145 | 146 | if pinned { 147 | self.repo.pinTicker(symbol: ticker.symbol) 148 | } else { 149 | self.repo.unpinTicker(symbol: ticker.symbol) 150 | } 151 | } 152 | 153 | func pinButtonState(row: Int) -> Int { 154 | let ticker = getTicker(row: row) 155 | return ticker.isPinned ? 1 : 0 156 | } 157 | 158 | func filterData(query: String) { 159 | if query.isEmpty { 160 | // Show all values 161 | self.filteredData.value = self.data 162 | self.reloadDataCallback?() 163 | 164 | return 165 | } 166 | 167 | self.filteredData.value = self.data.filter { 168 | $0.name.lowercased().contains(query.lowercased()) 169 | || $0.symbol.lowercased().contains(query.lowercased()) 170 | } 171 | 172 | self.reloadDataCallback?() 173 | } 174 | 175 | func refresh() { 176 | self.isRefreshing.value = true 177 | 178 | self.repo.refreshAllTickers().subscribe(onNext: { _ in 179 | self.isRefreshing.value = false 180 | }, onError: { _ in 181 | self.isRefreshing.value = false 182 | }) 183 | .addDisposableTo(disposeBag) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Nova/repository/DataRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // DataRepository.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | 22 | import Foundation 23 | import RxSwift 24 | import RealmSwift 25 | 26 | /// Central data source that abstracts fetching and caching of app data 27 | class DataRepository { 28 | 29 | private var local: LocalDataSource 30 | 31 | private var remote: RemoteDataSource 32 | 33 | private var prefs: Prefs 34 | 35 | private var tickerUpdateSubscription: Disposable? 36 | 37 | private var globalRefreshSubscription: Disposable? 38 | 39 | private var disposeBag = DisposeBag() 40 | 41 | private var refreshSubscriptions = DisposeBag() 42 | 43 | private var tickerUpdateTimestamp: Date? 44 | 45 | init(local: LocalDataSource, remote: RemoteDataSource, prefs: Prefs) { 46 | self.local = local 47 | self.remote = remote 48 | self.prefs = prefs 49 | } 50 | 51 | /// Get pinned tickers sorted by orderIndex 52 | func getPinnedTickers() -> Observable<[Ticker]> { 53 | return self.local.getPinnedTickers() 54 | } 55 | 56 | /// Set ticker as pinned (show in menu bar) 57 | func pinTicker(symbol: String) { 58 | self.local.pinTicker(symbol: symbol) 59 | } 60 | 61 | /// Unpin ticker (remove from menu bar) 62 | func unpinTicker(symbol: String) { 63 | self.local.unpinTicker(symbol: symbol) 64 | } 65 | 66 | /// Get all cached tickers from Realm 67 | /// 68 | /// - Query remote repository for update and cache into Realm 69 | /// - Changes in Realm will automatically trigger update of UI 70 | /// - Sorted by market cap 71 | /// 72 | /// - returns: 73 | /// An observable of type `Ticker` 74 | func getAllTickers() -> Observable<[Ticker]> { 75 | self.remote.getAllTickers() 76 | .observeOn(MainScheduler.instance) 77 | .subscribe(onNext: { tickers in 78 | self.local.saveTickers(tickers: tickers) 79 | }) 80 | .addDisposableTo(disposeBag) 81 | 82 | return self.local.getAllTickers() 83 | } 84 | 85 | /// Get all tickers from remote repository and cache response into Realm 86 | /// 87 | /// - returns: 88 | /// An empty observable to notify the UI when finished 89 | func refreshAllTickers() -> Observable { 90 | return self.remote.getAllTickers() 91 | // Cache into Realm 92 | .do(onNext: { response in 93 | self.local.saveTickers(tickers: response) 94 | }) 95 | // Flat map to empty observable since we don't need the result 96 | .observeOn(MainScheduler.instance) 97 | .flatMap { _ in 98 | return Observable.just() 99 | } 100 | } 101 | 102 | /// Get top N (limit) tickers, sorted by market cap 103 | func getTopTickers(limit: Int) -> Observable<[Ticker]> { 104 | 105 | return self.remote.getTopTickers(limit: limit) 106 | } 107 | 108 | 109 | /// Request fresh prices from CryptoCompare api 110 | /// Update local repository on response 111 | /// 112 | /// - parameters: 113 | /// - baseSymbols: The array of crypto currency symbols (BTC, ETH, XRP...) 114 | /// - refreshInterval: 115 | /// The refresh interval in seconds. Default value is 15 seconds. 116 | func subscribeForTickerUpdates(baseSymbols: [String], refreshInterval: Float = 15.0) { 117 | print("Subscribe for ticker updates") 118 | self.tickerUpdateSubscription?.dispose() 119 | 120 | self.tickerUpdateSubscription = 121 | Observable 122 | .timer(0, period: RxTimeInterval(refreshInterval), scheduler: Schedulers.background) 123 | // Query CryptoCompare api for an update 124 | .flatMap({ _ -> Observable<[String: [String: Double]]> in 125 | return self.remote.getTickers(base: baseSymbols, target: [self.prefs.targetCurrency]) 126 | }) 127 | .retryWithDelay(timeInterval: Int(refreshInterval)) 128 | .subscribe(onNext: { tickers in 129 | let timestamp = DateFormatter.localizedString( 130 | from: NSDate() as Date, dateStyle: .none, timeStyle: .medium) 131 | 132 | print("Pinned tickers updated at: \(timestamp)") 133 | self.local.updateTickerPrices(tickers: tickers) 134 | }, onError: { error in 135 | print(error) 136 | }) 137 | 138 | self.tickerUpdateSubscription?.addDisposableTo(refreshSubscriptions) 139 | } 140 | 141 | /// Request fresh prices from CoinMarketCap 142 | /// Update local repository on response 143 | /// 144 | /// - parameters: 145 | /// - refreshIntervalMinutes: The refresh interval in minutes. 146 | /// Min. value is 5 minutes, since CMC endpoint refreshes every 5 minutes. 147 | func subscribeForGlobalUpdates(refreshIntervalMinutes: Int = 5) { 148 | print("Subscribe for global updates") 149 | guard refreshIntervalMinutes >= 5 else { 150 | fatalError("Do not use refrsh interval of less than 5 minutes." + 151 | "CoinMarketCap endpoint refreshes every 5 minutes") 152 | } 153 | 154 | self.globalRefreshSubscription?.dispose() 155 | 156 | self.globalRefreshSubscription = 157 | Observable.timer(0, period: RxTimeInterval(refreshIntervalMinutes * 60), scheduler: Schedulers.background) 158 | // Query Cryptonator api for an update 159 | .flatMap { _ -> Observable<[Ticker]> in 160 | 161 | return self.remote.getAllTickers() 162 | } 163 | .subscribe(onNext: { tickers in 164 | print("All tickers updated.") 165 | self.local.saveTickersAsync(tickers: tickers) 166 | }, onError: { error in 167 | print(error) 168 | }) 169 | 170 | self.globalRefreshSubscription?.addDisposableTo(refreshSubscriptions) 171 | } 172 | 173 | func disposeTickerSubscription() { 174 | self.tickerUpdateSubscription?.dispose() 175 | } 176 | 177 | /// Terminate all running refresh subscriptions 178 | func disposeRefreshSubscriptions() { 179 | self.refreshSubscriptions = DisposeBag() 180 | } 181 | 182 | func getNews() -> Observable { 183 | return self.remote.getNews() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Nova/view/tickerlist/TickerListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 Andrej Jurkin. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | // TickerListViewController.swift 18 | // Nova 19 | // 20 | // Created by Andrej Jurkin on 9/3/17. 21 | // 22 | 23 | import Cocoa 24 | import RxSwift 25 | import RxCocoa 26 | import Kingfisher 27 | 28 | class TickerListViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { 29 | 30 | @IBOutlet weak var searchTextField: NSTextField! 31 | @IBOutlet weak var tickerTableView: NSTableView! 32 | @IBOutlet weak var refreshButton: NSButton! 33 | @IBOutlet var settingsMenu: NSMenu! 34 | @IBOutlet weak var targetCurrencies: NSMenu! 35 | @IBOutlet weak var menuBarFormats: NSMenu! 36 | 37 | // News 38 | @IBOutlet weak var newsContainer: NSBox! 39 | @IBOutlet weak var newsTitle: NSTextField! 40 | @IBOutlet weak var newsSubtitle: NSTextField! 41 | @IBOutlet weak var newsButton: NSTextField! 42 | @IBOutlet weak var newsContainerHeightConstraint: NSLayoutConstraint! 43 | @IBOutlet weak var tickerListTopConstraint: NSLayoutConstraint! 44 | 45 | var viewModel = Injector.inject(type: TickerListViewModel.self) 46 | var disposeBag = DisposeBag() 47 | 48 | override func viewDidLoad() { 49 | super.viewDidLoad() 50 | 51 | self.searchTextField.rx.text.orEmpty 52 | .bind(to: viewModel.searchString) 53 | .addDisposableTo(disposeBag) 54 | 55 | self.viewModel.reloadDataCallback = { 56 | self.tickerTableView.reloadData() 57 | } 58 | 59 | // Bind refresh button 60 | self.viewModel.isRefreshing.asObservable().subscribe { refreshingState in 61 | guard let isRefreshing = refreshingState.element else { 62 | return 63 | } 64 | 65 | if isRefreshing { 66 | self.refreshButton.isEnabled = false 67 | self.startRefreshAnimation() 68 | } else { 69 | self.refreshButton.isEnabled = true 70 | self.stopRefreshAnimation() 71 | } 72 | } 73 | .addDisposableTo(disposeBag) 74 | 75 | // Set placeholder color 76 | if let font = NSFont(name: "Avenir-Book", size: 17) { 77 | let attributes = [NSForegroundColorAttributeName: R.Color.placeholderLight, NSFontAttributeName: font] 78 | 79 | let searchPlaceholder = NSAttributedString(string: "Search...", attributes: attributes) 80 | self.searchTextField.placeholderAttributedString = searchPlaceholder 81 | } 82 | 83 | // Bind menu item actions 84 | let targetCurrency = self.viewModel.displayCurrency 85 | for menu in targetCurrencies.items { 86 | menu.target = self 87 | menu.action = #selector(onTargetCurrencyClick(sender:)) 88 | 89 | if targetCurrency == menu.title { 90 | menu.state = NSOnState 91 | } 92 | } 93 | 94 | let menuBarFormat = self.viewModel.menuBarFormat 95 | for menu in menuBarFormats.items { 96 | if !menu.isEnabled { 97 | continue 98 | } 99 | 100 | menu.target = self 101 | menu.action = #selector(onMenuBarFormatClick(sender:)) 102 | 103 | if menuBarFormat == menu.identifier { 104 | menu.state = NSOnState 105 | } 106 | } 107 | } 108 | 109 | override var representedObject: Any? { 110 | 111 | didSet { 112 | // Update the view, if already loaded. 113 | } 114 | } 115 | 116 | override func viewDidAppear() { 117 | super.viewDidAppear() 118 | self.view.window?.titleVisibility = .hidden 119 | self.view.window?.titlebarAppearsTransparent = true 120 | self.view.window?.styleMask.insert(.fullSizeContentView) 121 | self.view.window?.isOpaque = false 122 | self.view.window?.backgroundColor = NSColor.clear 123 | self.searchTextField.cursorColor = R.Color.primaryLight 124 | 125 | // Bind filtered data 126 | self.viewModel.filteredData 127 | .asObservable() 128 | .subscribe(onNext: { _ in 129 | self.tickerTableView.reloadData() 130 | }) 131 | .addDisposableTo(disposeBag) 132 | 133 | // Bind news 134 | self.viewModel.news 135 | .asObservable() 136 | .subscribe({ news in 137 | guard let news = news.element else { 138 | return 139 | } 140 | if (news.hidden) { 141 | self.hideNewsContainer() 142 | } else { 143 | self.showNewsContainer() 144 | self.newsContainer.isHidden = news.hidden 145 | self.newsTitle.stringValue = news.title 146 | self.newsSubtitle.stringValue = news.subtitle 147 | self.newsButton.stringValue = news.buttonTitle 148 | } 149 | }) 150 | .addDisposableTo(disposeBag) 151 | } 152 | 153 | @IBAction func onRefreshButtonClick(_ sender: Any) { 154 | self.refreshData() 155 | } 156 | 157 | @IBAction func onSettingsButtonClick(_ sender: Any) { 158 | self.settingsMenu.popUp(positioning: self.settingsMenu.item(at: 0), 159 | at: NSEvent.mouseLocation(), in: nil) 160 | } 161 | 162 | @IBAction func onLicenseClick(_ sender: AnyObject) { 163 | self.openPdf(resourceName: "license") 164 | } 165 | 166 | @IBAction func onAcknowledgementsClick(_ sender: AnyObject) { 167 | self.openPdf(resourceName: "acknowledgements") 168 | } 169 | 170 | @IBAction func openSupportWebsite(_ sender: Any) { 171 | if let url = URL(string: "https://github.com/AndrejJurkin/nova-osx") { 172 | NSWorkspace.shared().open(url) 173 | } 174 | } 175 | 176 | @IBAction func onNewsButtonClick(_ sender: Any) { 177 | if let url = URL(string: self.viewModel.news.value.link) { 178 | NSWorkspace.shared().open(url) 179 | AppDelegate.shared().menuBarView?.hidePopover() 180 | } 181 | } 182 | 183 | func refreshData() { 184 | self.viewModel.refresh() 185 | AppDelegate.shared().menuBarView?.refresh() 186 | } 187 | 188 | func onTargetCurrencyClick(sender: NSMenuItem) { 189 | sender.state = NSOnState 190 | 191 | for menuItem in targetCurrencies.items { 192 | if sender.title == menuItem.title { 193 | continue 194 | } 195 | 196 | menuItem.state = NSOffState 197 | } 198 | 199 | self.viewModel.displayCurrency = sender.title 200 | self.refreshData() 201 | } 202 | 203 | func onMenuBarFormatClick(sender: NSMenuItem) { 204 | sender.state = NSOnState 205 | 206 | for menuItem in menuBarFormats.items { 207 | if (sender.identifier == menuItem.identifier) || !menuItem.isEnabled { 208 | continue 209 | } 210 | 211 | menuItem.state = NSOffState 212 | } 213 | 214 | self.viewModel.menuBarFormat = sender.identifier! 215 | AppDelegate.shared().menuBarView?.refresh() 216 | } 217 | 218 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 219 | 220 | let view = tableView.make(withIdentifier: "Cell", owner: self) as! TickerListCellView 221 | 222 | view.currencyName.stringValue = self.viewModel.getCurrencyName(row: row) 223 | view.currencyImageView.kf.setImage(with: self.viewModel.getCurrencyImageUrl(row: row)) 224 | view.currencyPrice.stringValue = self.viewModel.getTargetPrice(row: row) 225 | 226 | view.pinButton.tag = row 227 | view.pinButton.target = self 228 | view.pinButton.action = #selector(onPinButtonClick(sender:)) 229 | view.pinButton.state = self.viewModel.pinButtonState(row: row) 230 | 231 | return view 232 | } 233 | 234 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { 235 | return 48.0 236 | } 237 | 238 | func numberOfRows(in tableView: NSTableView) -> Int { 239 | return self.viewModel.numberOfRows 240 | } 241 | 242 | func onPinButtonClick(sender: NSButton) { 243 | self.viewModel.pinStatusChanged(row: sender.tag, pinned: sender.state == 1) 244 | } 245 | 246 | func startRefreshAnimation() { 247 | let anim = CABasicAnimation(keyPath: "transform.rotation.z") 248 | anim.fromValue = 0 249 | anim.toValue = Double.pi * -2 250 | anim.duration = 0.75 251 | anim.repeatCount = HUGE 252 | 253 | if let frame = self.refreshButton.layer?.frame { 254 | let center = CGPoint(x: frame.midX, y: frame.midY) 255 | self.refreshButton.layer?.position = center 256 | self.refreshButton.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) 257 | self.refreshButton.layer?.add(anim, forKey: nil) 258 | } 259 | } 260 | 261 | func stopRefreshAnimation() { 262 | self.refreshButton.layer?.removeAllAnimations() 263 | } 264 | 265 | func openPdf(resourceName: String) { 266 | AppDelegate.shared().menuBarView?.hidePopover() 267 | 268 | if let pdfURL = Bundle.main.url(forResource: resourceName, withExtension: "pdf") { 269 | NSWorkspace.shared().open(pdfURL) 270 | } 271 | } 272 | 273 | func hideNewsContainer() { 274 | self.tickerListTopConstraint.constant = 0; 275 | } 276 | 277 | func showNewsContainer() { 278 | self.tickerListTopConstraint.constant = self.newsContainer.frame.size.height 279 | } 280 | } 281 | 282 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Nova.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0A2D3E361F5AC429002510BA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D3E351F5AC429002510BA /* AppDelegate.swift */; }; 11 | 0A2D3E381F5AC429002510BA /* TickerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D3E371F5AC429002510BA /* TickerListViewController.swift */; }; 12 | 0A2D3E3A1F5AC429002510BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0A2D3E391F5AC429002510BA /* Assets.xcassets */; }; 13 | 0A2D3E3D1F5AC429002510BA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0A2D3E3B1F5AC429002510BA /* Main.storyboard */; }; 14 | 0A2D3E481F5AC429002510BA /* NovaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D3E471F5AC429002510BA /* NovaTests.swift */; }; 15 | 0A2D3E551F5AFA2A002510BA /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D3E541F5AFA2A002510BA /* Constants.swift */; }; 16 | 0A2D3E5A1F5B4F24002510BA /* CoinMarketCapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D3E591F5B4F24002510BA /* CoinMarketCapProvider.swift */; }; 17 | 0A52028E1F65631E00F71C1E /* TickerListCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A52028D1F65631E00F71C1E /* TickerListCellView.swift */; }; 18 | 0A8F66381F766B2F00723F8F /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8F66371F766B2F00723F8F /* EventMonitor.swift */; }; 19 | 0A924C921F5C54D100A8875E /* TickerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A924C911F5C54D100A8875E /* TickerListViewModel.swift */; }; 20 | 0AA23C8C1FA6525A000FF632 /* PriceFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA23C8B1FA6525A000FF632 /* PriceFormatter.swift */; }; 21 | 0AB59B0A1F699CF400798BBB /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB59B091F699CF400798BBB /* Extensions.swift */; }; 22 | 0AB59B0C1F69A6AD00798BBB /* MenuBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB59B0B1F69A6AD00798BBB /* MenuBarView.swift */; }; 23 | 0AB59B0E1F69A6B300798BBB /* MenuBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB59B0D1F69A6B300798BBB /* MenuBarViewModel.swift */; }; 24 | 0AB59B111F69A80900798BBB /* Prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB59B101F69A80900798BBB /* Prefs.swift */; }; 25 | 0ACB1FA11F74F430009C3D2D /* CryptoCompareProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ACB1FA01F74F430009C3D2D /* CryptoCompareProvider.swift */; }; 26 | 0ACE435E1F769A41008059A7 /* license.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0ACE435D1F769A41008059A7 /* license.pdf */; }; 27 | 0ACE43601F769A5D008059A7 /* acknowledgements.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0ACE435F1F769A5D008059A7 /* acknowledgements.pdf */; }; 28 | 0AF23FA11F5BEC4200AB0D15 /* Ticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF23FA01F5BEC4200AB0D15 /* Ticker.swift */; }; 29 | 0AF549F71FF67D910097430E /* FirebaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF549F61FF67D910097430E /* FirebaseProvider.swift */; }; 30 | 0AF549F91FF796250097430E /* News.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF549F81FF796250097430E /* News.swift */; }; 31 | 771EBD911F72BE6F00F756ED /* Schedulers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EBD901F72BE6F00F756ED /* Schedulers.swift */; }; 32 | 7763D05B1F6FAA0C009EE03E /* RemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7763D05A1F6FAA0C009EE03E /* RemoteDataSource.swift */; }; 33 | 7763D05D1F6FAA18009EE03E /* LocalDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7763D05C1F6FAA18009EE03E /* LocalDataSource.swift */; }; 34 | 7763D0601F6FB683009EE03E /* Injector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7763D05F1F6FB683009EE03E /* Injector.swift */; }; 35 | 7763D0641F6FCE64009EE03E /* DataRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7763D0631F6FCE64009EE03E /* DataRepository.swift */; }; 36 | 7764E6621F7A5F3200FC67F0 /* ContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7764E6611F7A5F3200FC67F0 /* ContactViewController.swift */; }; 37 | 7764E6651F7A962900FC67F0 /* DonateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7764E6631F7A962900FC67F0 /* DonateViewController.swift */; }; 38 | D418FF040586A9DD73E9A005 /* Pods_Nova.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AC2ED320E03871B42E360B4 /* Pods_Nova.framework */; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXContainerItemProxy section */ 42 | 0A2D3E441F5AC429002510BA /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 0A2D3E2A1F5AC429002510BA /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = 0A2D3E311F5AC429002510BA; 47 | remoteInfo = Nova; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXFileReference section */ 52 | 0A2D3E321F5AC429002510BA /* Nova.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nova.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 0A2D3E351F5AC429002510BA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | 0A2D3E371F5AC429002510BA /* TickerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TickerListViewController.swift; sourceTree = ""; }; 55 | 0A2D3E391F5AC429002510BA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56 | 0A2D3E3C1F5AC429002510BA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 57 | 0A2D3E3E1F5AC429002510BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | 0A2D3E431F5AC429002510BA /* NovaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NovaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | 0A2D3E471F5AC429002510BA /* NovaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NovaTests.swift; sourceTree = ""; }; 60 | 0A2D3E491F5AC429002510BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | 0A2D3E541F5AFA2A002510BA /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 62 | 0A2D3E591F5B4F24002510BA /* CoinMarketCapProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinMarketCapProvider.swift; sourceTree = ""; }; 63 | 0A52028D1F65631E00F71C1E /* TickerListCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TickerListCellView.swift; sourceTree = ""; }; 64 | 0A8F66371F766B2F00723F8F /* EventMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventMonitor.swift; sourceTree = ""; }; 65 | 0A924C911F5C54D100A8875E /* TickerListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TickerListViewModel.swift; sourceTree = ""; }; 66 | 0AA23C8B1FA6525A000FF632 /* PriceFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceFormatter.swift; sourceTree = ""; }; 67 | 0AB59B091F699CF400798BBB /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 68 | 0AB59B0B1F69A6AD00798BBB /* MenuBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = ""; }; 69 | 0AB59B0D1F69A6B300798BBB /* MenuBarViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuBarViewModel.swift; sourceTree = ""; }; 70 | 0AB59B101F69A80900798BBB /* Prefs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Prefs.swift; sourceTree = ""; }; 71 | 0ACB1FA01F74F430009C3D2D /* CryptoCompareProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoCompareProvider.swift; sourceTree = ""; }; 72 | 0ACE435D1F769A41008059A7 /* license.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = license.pdf; sourceTree = ""; }; 73 | 0ACE435F1F769A5D008059A7 /* acknowledgements.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = acknowledgements.pdf; sourceTree = ""; }; 74 | 0AF23FA01F5BEC4200AB0D15 /* Ticker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ticker.swift; sourceTree = ""; }; 75 | 0AF549F61FF67D910097430E /* FirebaseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseProvider.swift; sourceTree = ""; }; 76 | 0AF549F81FF796250097430E /* News.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = News.swift; sourceTree = ""; }; 77 | 207457C11AEF35045A801412 /* Pods-Nova.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nova.release.xcconfig"; path = "Pods/Target Support Files/Pods-Nova/Pods-Nova.release.xcconfig"; sourceTree = ""; }; 78 | 4AC2ED320E03871B42E360B4 /* Pods_Nova.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Nova.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | 770C67611F8373A700D3D104 /* Nova.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nova.entitlements; sourceTree = ""; }; 80 | 771EBD901F72BE6F00F756ED /* Schedulers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Schedulers.swift; sourceTree = ""; }; 81 | 7763D05A1F6FAA0C009EE03E /* RemoteDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteDataSource.swift; sourceTree = ""; }; 82 | 7763D05C1F6FAA18009EE03E /* LocalDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalDataSource.swift; sourceTree = ""; }; 83 | 7763D05F1F6FB683009EE03E /* Injector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Injector.swift; sourceTree = ""; }; 84 | 7763D0631F6FCE64009EE03E /* DataRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataRepository.swift; sourceTree = ""; }; 85 | 7764E6611F7A5F3200FC67F0 /* ContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactViewController.swift; sourceTree = ""; }; 86 | 7764E6631F7A962900FC67F0 /* DonateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonateViewController.swift; sourceTree = ""; }; 87 | C31629810B46A92A7F784CDD /* Pods-Nova.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nova.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Nova/Pods-Nova.debug.xcconfig"; sourceTree = ""; }; 88 | DB43C4AF6E52B7B34E2E6C3C /* Pods_NovaTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NovaTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | /* End PBXFileReference section */ 90 | 91 | /* Begin PBXFrameworksBuildPhase section */ 92 | 0A2D3E2F1F5AC429002510BA /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | D418FF040586A9DD73E9A005 /* Pods_Nova.framework in Frameworks */, 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | 0A2D3E401F5AC429002510BA /* Frameworks */ = { 101 | isa = PBXFrameworksBuildPhase; 102 | buildActionMask = 2147483647; 103 | files = ( 104 | ); 105 | runOnlyForDeploymentPostprocessing = 0; 106 | }; 107 | /* End PBXFrameworksBuildPhase section */ 108 | 109 | /* Begin PBXGroup section */ 110 | 0A2D3E291F5AC429002510BA = { 111 | isa = PBXGroup; 112 | children = ( 113 | F2B85A38647AABC95B5FF6C8 /* Frameworks */, 114 | 0A2D3E341F5AC429002510BA /* Nova */, 115 | 0A2D3E461F5AC429002510BA /* NovaTests */, 116 | E32EDFE3368E06439B9064F1 /* Pods */, 117 | 0A2D3E331F5AC429002510BA /* Products */, 118 | ); 119 | sourceTree = ""; 120 | }; 121 | 0A2D3E331F5AC429002510BA /* Products */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 0A2D3E321F5AC429002510BA /* Nova.app */, 125 | 0A2D3E431F5AC429002510BA /* NovaTests.xctest */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | 0A2D3E341F5AC429002510BA /* Nova */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 0A2D3E561F5B4EAE002510BA /* data */, 134 | 7763D0561F6FA888009EE03E /* repository */, 135 | 7763D0651F6FD043009EE03E /* support */, 136 | 0A924C8F1F5C516300A8875E /* view */, 137 | 0A2D3E351F5AC429002510BA /* AppDelegate.swift */, 138 | 0A2D3E391F5AC429002510BA /* Assets.xcassets */, 139 | 0A2D3E3E1F5AC429002510BA /* Info.plist */, 140 | 7763D05F1F6FB683009EE03E /* Injector.swift */, 141 | 0A2D3E3B1F5AC429002510BA /* Main.storyboard */, 142 | 770C67611F8373A700D3D104 /* Nova.entitlements */, 143 | ); 144 | path = Nova; 145 | sourceTree = ""; 146 | }; 147 | 0A2D3E461F5AC429002510BA /* NovaTests */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 0A2D3E491F5AC429002510BA /* Info.plist */, 151 | 0A2D3E471F5AC429002510BA /* NovaTests.swift */, 152 | ); 153 | path = NovaTests; 154 | sourceTree = ""; 155 | }; 156 | 0A2D3E561F5B4EAE002510BA /* data */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 0AF549F51FF67D6E0097430E /* firebase */, 160 | 777467491F681E5A00F01AF3 /* coinmarketcap */, 161 | 0ACB1F9E1F74F296009C3D2D /* cryptocompare */, 162 | ); 163 | path = data; 164 | sourceTree = ""; 165 | }; 166 | 0A924C8F1F5C516300A8875E /* view */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 0AB59B0F1F69A6B600798BBB /* menubar */, 170 | 0A924C901F5C54BC00A8875E /* tickerlist */, 171 | 7764E6611F7A5F3200FC67F0 /* ContactViewController.swift */, 172 | 7764E6631F7A962900FC67F0 /* DonateViewController.swift */, 173 | ); 174 | path = view; 175 | sourceTree = ""; 176 | }; 177 | 0A924C901F5C54BC00A8875E /* tickerlist */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 0A52028D1F65631E00F71C1E /* TickerListCellView.swift */, 181 | 0A2D3E371F5AC429002510BA /* TickerListViewController.swift */, 182 | 0A924C911F5C54D100A8875E /* TickerListViewModel.swift */, 183 | ); 184 | path = tickerlist; 185 | sourceTree = ""; 186 | }; 187 | 0AB59B0F1F69A6B600798BBB /* menubar */ = { 188 | isa = PBXGroup; 189 | children = ( 190 | 0AB59B0B1F69A6AD00798BBB /* MenuBarView.swift */, 191 | 0AB59B0D1F69A6B300798BBB /* MenuBarViewModel.swift */, 192 | ); 193 | path = menubar; 194 | sourceTree = ""; 195 | }; 196 | 0ACB1F9E1F74F296009C3D2D /* cryptocompare */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 0ACB1FA01F74F430009C3D2D /* CryptoCompareProvider.swift */, 200 | ); 201 | path = cryptocompare; 202 | sourceTree = ""; 203 | }; 204 | 0AF549F51FF67D6E0097430E /* firebase */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 0AF549F61FF67D910097430E /* FirebaseProvider.swift */, 208 | 0AF549F81FF796250097430E /* News.swift */, 209 | ); 210 | path = firebase; 211 | sourceTree = ""; 212 | }; 213 | 0AF9200E1F76935B00C62EBA /* pdf */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | 0ACE435F1F769A5D008059A7 /* acknowledgements.pdf */, 217 | 0ACE435D1F769A41008059A7 /* license.pdf */, 218 | ); 219 | path = pdf; 220 | sourceTree = ""; 221 | }; 222 | 7763D0561F6FA888009EE03E /* repository */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | 7763D0631F6FCE64009EE03E /* DataRepository.swift */, 226 | 7763D05C1F6FAA18009EE03E /* LocalDataSource.swift */, 227 | 7763D05A1F6FAA0C009EE03E /* RemoteDataSource.swift */, 228 | ); 229 | path = repository; 230 | sourceTree = ""; 231 | }; 232 | 7763D0651F6FD043009EE03E /* support */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | 0AF9200E1F76935B00C62EBA /* pdf */, 236 | 0A2D3E541F5AFA2A002510BA /* Constants.swift */, 237 | 0A8F66371F766B2F00723F8F /* EventMonitor.swift */, 238 | 0AB59B091F699CF400798BBB /* Extensions.swift */, 239 | 0AB59B101F69A80900798BBB /* Prefs.swift */, 240 | 0AA23C8B1FA6525A000FF632 /* PriceFormatter.swift */, 241 | 771EBD901F72BE6F00F756ED /* Schedulers.swift */, 242 | ); 243 | path = support; 244 | sourceTree = ""; 245 | }; 246 | 777467491F681E5A00F01AF3 /* coinmarketcap */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | 0A2D3E591F5B4F24002510BA /* CoinMarketCapProvider.swift */, 250 | 0AF23FA01F5BEC4200AB0D15 /* Ticker.swift */, 251 | ); 252 | path = coinmarketcap; 253 | sourceTree = ""; 254 | }; 255 | E32EDFE3368E06439B9064F1 /* Pods */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | C31629810B46A92A7F784CDD /* Pods-Nova.debug.xcconfig */, 259 | 207457C11AEF35045A801412 /* Pods-Nova.release.xcconfig */, 260 | ); 261 | name = Pods; 262 | sourceTree = ""; 263 | }; 264 | F2B85A38647AABC95B5FF6C8 /* Frameworks */ = { 265 | isa = PBXGroup; 266 | children = ( 267 | 4AC2ED320E03871B42E360B4 /* Pods_Nova.framework */, 268 | DB43C4AF6E52B7B34E2E6C3C /* Pods_NovaTests.framework */, 269 | ); 270 | name = Frameworks; 271 | sourceTree = ""; 272 | }; 273 | /* End PBXGroup section */ 274 | 275 | /* Begin PBXNativeTarget section */ 276 | 0A2D3E311F5AC429002510BA /* Nova */ = { 277 | isa = PBXNativeTarget; 278 | buildConfigurationList = 0A2D3E4C1F5AC429002510BA /* Build configuration list for PBXNativeTarget "Nova" */; 279 | buildPhases = ( 280 | D325D3843B7EE25F13F42E0F /* [CP] Check Pods Manifest.lock */, 281 | 0A2D3E2E1F5AC429002510BA /* Sources */, 282 | 0A2D3E2F1F5AC429002510BA /* Frameworks */, 283 | 0A2D3E301F5AC429002510BA /* Resources */, 284 | 122A1166610F127D6296BD09 /* [CP] Embed Pods Frameworks */, 285 | 5787E495D0977E4A24807FAD /* [CP] Copy Pods Resources */, 286 | ); 287 | buildRules = ( 288 | ); 289 | dependencies = ( 290 | ); 291 | name = Nova; 292 | productName = Nova; 293 | productReference = 0A2D3E321F5AC429002510BA /* Nova.app */; 294 | productType = "com.apple.product-type.application"; 295 | }; 296 | 0A2D3E421F5AC429002510BA /* NovaTests */ = { 297 | isa = PBXNativeTarget; 298 | buildConfigurationList = 0A2D3E4F1F5AC429002510BA /* Build configuration list for PBXNativeTarget "NovaTests" */; 299 | buildPhases = ( 300 | 0A2D3E3F1F5AC429002510BA /* Sources */, 301 | 0A2D3E401F5AC429002510BA /* Frameworks */, 302 | 0A2D3E411F5AC429002510BA /* Resources */, 303 | ); 304 | buildRules = ( 305 | ); 306 | dependencies = ( 307 | 0A2D3E451F5AC429002510BA /* PBXTargetDependency */, 308 | ); 309 | name = NovaTests; 310 | productName = NovaTests; 311 | productReference = 0A2D3E431F5AC429002510BA /* NovaTests.xctest */; 312 | productType = "com.apple.product-type.bundle.unit-test"; 313 | }; 314 | /* End PBXNativeTarget section */ 315 | 316 | /* Begin PBXProject section */ 317 | 0A2D3E2A1F5AC429002510BA /* Project object */ = { 318 | isa = PBXProject; 319 | attributes = { 320 | LastSwiftUpdateCheck = 0800; 321 | LastUpgradeCheck = 0800; 322 | ORGANIZATIONNAME = "Andrej Jurkin"; 323 | TargetAttributes = { 324 | 0A2D3E311F5AC429002510BA = { 325 | CreatedOnToolsVersion = 8.0; 326 | DevelopmentTeam = XJF3S8KP2L; 327 | ProvisioningStyle = Automatic; 328 | SystemCapabilities = { 329 | com.apple.Sandbox = { 330 | enabled = 1; 331 | }; 332 | }; 333 | }; 334 | 0A2D3E421F5AC429002510BA = { 335 | CreatedOnToolsVersion = 8.0; 336 | DevelopmentTeam = XJF3S8KP2L; 337 | ProvisioningStyle = Automatic; 338 | TestTargetID = 0A2D3E311F5AC429002510BA; 339 | }; 340 | }; 341 | }; 342 | buildConfigurationList = 0A2D3E2D1F5AC429002510BA /* Build configuration list for PBXProject "Nova" */; 343 | compatibilityVersion = "Xcode 3.2"; 344 | developmentRegion = English; 345 | hasScannedForEncodings = 0; 346 | knownRegions = ( 347 | en, 348 | Base, 349 | ); 350 | mainGroup = 0A2D3E291F5AC429002510BA; 351 | productRefGroup = 0A2D3E331F5AC429002510BA /* Products */; 352 | projectDirPath = ""; 353 | projectRoot = ""; 354 | targets = ( 355 | 0A2D3E311F5AC429002510BA /* Nova */, 356 | 0A2D3E421F5AC429002510BA /* NovaTests */, 357 | ); 358 | }; 359 | /* End PBXProject section */ 360 | 361 | /* Begin PBXResourcesBuildPhase section */ 362 | 0A2D3E301F5AC429002510BA /* Resources */ = { 363 | isa = PBXResourcesBuildPhase; 364 | buildActionMask = 2147483647; 365 | files = ( 366 | 0A2D3E3A1F5AC429002510BA /* Assets.xcassets in Resources */, 367 | 0ACE435E1F769A41008059A7 /* license.pdf in Resources */, 368 | 0A2D3E3D1F5AC429002510BA /* Main.storyboard in Resources */, 369 | 0ACE43601F769A5D008059A7 /* acknowledgements.pdf in Resources */, 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | }; 373 | 0A2D3E411F5AC429002510BA /* Resources */ = { 374 | isa = PBXResourcesBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | /* End PBXResourcesBuildPhase section */ 381 | 382 | /* Begin PBXShellScriptBuildPhase section */ 383 | 122A1166610F127D6296BD09 /* [CP] Embed Pods Frameworks */ = { 384 | isa = PBXShellScriptBuildPhase; 385 | buildActionMask = 2147483647; 386 | files = ( 387 | ); 388 | inputPaths = ( 389 | "${SRCROOT}/Pods/Target Support Files/Pods-Nova/Pods-Nova-frameworks.sh", 390 | "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", 391 | "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework", 392 | "${BUILT_PRODUCTS_DIR}/Moya/Moya.framework", 393 | "${BUILT_PRODUCTS_DIR}/Moya-ObjectMapper/Moya_ObjectMapper.framework", 394 | "${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework", 395 | "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", 396 | "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", 397 | "${BUILT_PRODUCTS_DIR}/Result/Result.framework", 398 | "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework", 399 | "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", 400 | "${BUILT_PRODUCTS_DIR}/RxOptional/RxOptional.framework", 401 | "${BUILT_PRODUCTS_DIR}/RxRealm/RxRealm.framework", 402 | "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", 403 | "${BUILT_PRODUCTS_DIR}/Swinject/Swinject.framework", 404 | "${BUILT_PRODUCTS_DIR}/SwinjectAutoregistration/SwinjectAutoregistration.framework", 405 | ); 406 | name = "[CP] Embed Pods Frameworks"; 407 | outputPaths = ( 408 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", 409 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework", 410 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Moya.framework", 411 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Moya_ObjectMapper.framework", 412 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ObjectMapper.framework", 413 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", 414 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", 415 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Result.framework", 416 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAlamofire.framework", 417 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", 418 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxOptional.framework", 419 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRealm.framework", 420 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", 421 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swinject.framework", 422 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwinjectAutoregistration.framework", 423 | ); 424 | runOnlyForDeploymentPostprocessing = 0; 425 | shellPath = /bin/sh; 426 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Nova/Pods-Nova-frameworks.sh\"\n"; 427 | showEnvVarsInLog = 0; 428 | }; 429 | 5787E495D0977E4A24807FAD /* [CP] Copy Pods Resources */ = { 430 | isa = PBXShellScriptBuildPhase; 431 | buildActionMask = 2147483647; 432 | files = ( 433 | ); 434 | inputPaths = ( 435 | ); 436 | name = "[CP] Copy Pods Resources"; 437 | outputPaths = ( 438 | ); 439 | runOnlyForDeploymentPostprocessing = 0; 440 | shellPath = /bin/sh; 441 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Nova/Pods-Nova-resources.sh\"\n"; 442 | showEnvVarsInLog = 0; 443 | }; 444 | D325D3843B7EE25F13F42E0F /* [CP] Check Pods Manifest.lock */ = { 445 | isa = PBXShellScriptBuildPhase; 446 | buildActionMask = 2147483647; 447 | files = ( 448 | ); 449 | inputPaths = ( 450 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 451 | "${PODS_ROOT}/Manifest.lock", 452 | ); 453 | name = "[CP] Check Pods Manifest.lock"; 454 | outputPaths = ( 455 | "$(DERIVED_FILE_DIR)/Pods-Nova-checkManifestLockResult.txt", 456 | ); 457 | runOnlyForDeploymentPostprocessing = 0; 458 | shellPath = /bin/sh; 459 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 460 | showEnvVarsInLog = 0; 461 | }; 462 | /* End PBXShellScriptBuildPhase section */ 463 | 464 | /* Begin PBXSourcesBuildPhase section */ 465 | 0A2D3E2E1F5AC429002510BA /* Sources */ = { 466 | isa = PBXSourcesBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | 0AB59B111F69A80900798BBB /* Prefs.swift in Sources */, 470 | 0AF549F91FF796250097430E /* News.swift in Sources */, 471 | 771EBD911F72BE6F00F756ED /* Schedulers.swift in Sources */, 472 | 0A2D3E551F5AFA2A002510BA /* Constants.swift in Sources */, 473 | 0A2D3E381F5AC429002510BA /* TickerListViewController.swift in Sources */, 474 | 0ACB1FA11F74F430009C3D2D /* CryptoCompareProvider.swift in Sources */, 475 | 0AB59B0E1F69A6B300798BBB /* MenuBarViewModel.swift in Sources */, 476 | 0AF23FA11F5BEC4200AB0D15 /* Ticker.swift in Sources */, 477 | 7764E6621F7A5F3200FC67F0 /* ContactViewController.swift in Sources */, 478 | 7763D05D1F6FAA18009EE03E /* LocalDataSource.swift in Sources */, 479 | 7763D05B1F6FAA0C009EE03E /* RemoteDataSource.swift in Sources */, 480 | 0A8F66381F766B2F00723F8F /* EventMonitor.swift in Sources */, 481 | 0A52028E1F65631E00F71C1E /* TickerListCellView.swift in Sources */, 482 | 0A2D3E5A1F5B4F24002510BA /* CoinMarketCapProvider.swift in Sources */, 483 | 0A2D3E361F5AC429002510BA /* AppDelegate.swift in Sources */, 484 | 7763D0601F6FB683009EE03E /* Injector.swift in Sources */, 485 | 0AB59B0C1F69A6AD00798BBB /* MenuBarView.swift in Sources */, 486 | 0AA23C8C1FA6525A000FF632 /* PriceFormatter.swift in Sources */, 487 | 0AF549F71FF67D910097430E /* FirebaseProvider.swift in Sources */, 488 | 7763D0641F6FCE64009EE03E /* DataRepository.swift in Sources */, 489 | 7764E6651F7A962900FC67F0 /* DonateViewController.swift in Sources */, 490 | 0AB59B0A1F699CF400798BBB /* Extensions.swift in Sources */, 491 | 0A924C921F5C54D100A8875E /* TickerListViewModel.swift in Sources */, 492 | ); 493 | runOnlyForDeploymentPostprocessing = 0; 494 | }; 495 | 0A2D3E3F1F5AC429002510BA /* Sources */ = { 496 | isa = PBXSourcesBuildPhase; 497 | buildActionMask = 2147483647; 498 | files = ( 499 | 0A2D3E481F5AC429002510BA /* NovaTests.swift in Sources */, 500 | ); 501 | runOnlyForDeploymentPostprocessing = 0; 502 | }; 503 | /* End PBXSourcesBuildPhase section */ 504 | 505 | /* Begin PBXTargetDependency section */ 506 | 0A2D3E451F5AC429002510BA /* PBXTargetDependency */ = { 507 | isa = PBXTargetDependency; 508 | target = 0A2D3E311F5AC429002510BA /* Nova */; 509 | targetProxy = 0A2D3E441F5AC429002510BA /* PBXContainerItemProxy */; 510 | }; 511 | /* End PBXTargetDependency section */ 512 | 513 | /* Begin PBXVariantGroup section */ 514 | 0A2D3E3B1F5AC429002510BA /* Main.storyboard */ = { 515 | isa = PBXVariantGroup; 516 | children = ( 517 | 0A2D3E3C1F5AC429002510BA /* Base */, 518 | ); 519 | name = Main.storyboard; 520 | path = .; 521 | sourceTree = ""; 522 | }; 523 | /* End PBXVariantGroup section */ 524 | 525 | /* Begin XCBuildConfiguration section */ 526 | 0A2D3E4A1F5AC429002510BA /* Debug */ = { 527 | isa = XCBuildConfiguration; 528 | buildSettings = { 529 | ALWAYS_SEARCH_USER_PATHS = NO; 530 | CLANG_ANALYZER_NONNULL = YES; 531 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 532 | CLANG_CXX_LIBRARY = "libc++"; 533 | CLANG_ENABLE_MODULES = YES; 534 | CLANG_ENABLE_OBJC_ARC = YES; 535 | CLANG_WARN_BOOL_CONVERSION = YES; 536 | CLANG_WARN_CONSTANT_CONVERSION = YES; 537 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 538 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 539 | CLANG_WARN_EMPTY_BODY = YES; 540 | CLANG_WARN_ENUM_CONVERSION = YES; 541 | CLANG_WARN_INFINITE_RECURSION = YES; 542 | CLANG_WARN_INT_CONVERSION = YES; 543 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 544 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 545 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 546 | CLANG_WARN_UNREACHABLE_CODE = YES; 547 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 548 | CODE_SIGN_IDENTITY = "-"; 549 | COPY_PHASE_STRIP = NO; 550 | DEBUG_INFORMATION_FORMAT = dwarf; 551 | ENABLE_STRICT_OBJC_MSGSEND = YES; 552 | ENABLE_TESTABILITY = YES; 553 | GCC_C_LANGUAGE_STANDARD = gnu99; 554 | GCC_DYNAMIC_NO_PIC = NO; 555 | GCC_NO_COMMON_BLOCKS = YES; 556 | GCC_OPTIMIZATION_LEVEL = 0; 557 | GCC_PREPROCESSOR_DEFINITIONS = ( 558 | "DEBUG=1", 559 | "$(inherited)", 560 | ); 561 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 562 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 563 | GCC_WARN_UNDECLARED_SELECTOR = YES; 564 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 565 | GCC_WARN_UNUSED_FUNCTION = YES; 566 | GCC_WARN_UNUSED_VARIABLE = YES; 567 | MACOSX_DEPLOYMENT_TARGET = 10.11; 568 | MTL_ENABLE_DEBUG_INFO = YES; 569 | ONLY_ACTIVE_ARCH = YES; 570 | SDKROOT = macosx; 571 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 572 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 573 | }; 574 | name = Debug; 575 | }; 576 | 0A2D3E4B1F5AC429002510BA /* Release */ = { 577 | isa = XCBuildConfiguration; 578 | buildSettings = { 579 | ALWAYS_SEARCH_USER_PATHS = NO; 580 | CLANG_ANALYZER_NONNULL = YES; 581 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 582 | CLANG_CXX_LIBRARY = "libc++"; 583 | CLANG_ENABLE_MODULES = YES; 584 | CLANG_ENABLE_OBJC_ARC = YES; 585 | CLANG_WARN_BOOL_CONVERSION = YES; 586 | CLANG_WARN_CONSTANT_CONVERSION = YES; 587 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 588 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 589 | CLANG_WARN_EMPTY_BODY = YES; 590 | CLANG_WARN_ENUM_CONVERSION = YES; 591 | CLANG_WARN_INFINITE_RECURSION = YES; 592 | CLANG_WARN_INT_CONVERSION = YES; 593 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 594 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 595 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 596 | CLANG_WARN_UNREACHABLE_CODE = YES; 597 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 598 | CODE_SIGN_IDENTITY = "-"; 599 | COPY_PHASE_STRIP = NO; 600 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 601 | ENABLE_NS_ASSERTIONS = NO; 602 | ENABLE_STRICT_OBJC_MSGSEND = YES; 603 | GCC_C_LANGUAGE_STANDARD = gnu99; 604 | GCC_NO_COMMON_BLOCKS = YES; 605 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 606 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 607 | GCC_WARN_UNDECLARED_SELECTOR = YES; 608 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 609 | GCC_WARN_UNUSED_FUNCTION = YES; 610 | GCC_WARN_UNUSED_VARIABLE = YES; 611 | MACOSX_DEPLOYMENT_TARGET = 10.11; 612 | MTL_ENABLE_DEBUG_INFO = NO; 613 | SDKROOT = macosx; 614 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 615 | }; 616 | name = Release; 617 | }; 618 | 0A2D3E4D1F5AC429002510BA /* Debug */ = { 619 | isa = XCBuildConfiguration; 620 | baseConfigurationReference = C31629810B46A92A7F784CDD /* Pods-Nova.debug.xcconfig */; 621 | buildSettings = { 622 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 623 | CODE_SIGN_ENTITLEMENTS = Nova/Nova.entitlements; 624 | CODE_SIGN_IDENTITY = "Mac Developer"; 625 | COMBINE_HIDPI_IMAGES = YES; 626 | DEVELOPMENT_TEAM = XJF3S8KP2L; 627 | INFOPLIST_FILE = Nova/Info.plist; 628 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 629 | PRODUCT_BUNDLE_IDENTIFIER = jurkin.osx.Nova; 630 | PRODUCT_NAME = "$(TARGET_NAME)"; 631 | SWIFT_VERSION = 3.0; 632 | }; 633 | name = Debug; 634 | }; 635 | 0A2D3E4E1F5AC429002510BA /* Release */ = { 636 | isa = XCBuildConfiguration; 637 | baseConfigurationReference = 207457C11AEF35045A801412 /* Pods-Nova.release.xcconfig */; 638 | buildSettings = { 639 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 640 | CODE_SIGN_ENTITLEMENTS = Nova/Nova.entitlements; 641 | CODE_SIGN_IDENTITY = "Mac Developer"; 642 | COMBINE_HIDPI_IMAGES = YES; 643 | DEVELOPMENT_TEAM = XJF3S8KP2L; 644 | INFOPLIST_FILE = Nova/Info.plist; 645 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 646 | PRODUCT_BUNDLE_IDENTIFIER = jurkin.osx.Nova; 647 | PRODUCT_NAME = "$(TARGET_NAME)"; 648 | SWIFT_VERSION = 3.0; 649 | }; 650 | name = Release; 651 | }; 652 | 0A2D3E501F5AC429002510BA /* Debug */ = { 653 | isa = XCBuildConfiguration; 654 | buildSettings = { 655 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 656 | BUNDLE_LOADER = "$(TEST_HOST)"; 657 | COMBINE_HIDPI_IMAGES = YES; 658 | DEVELOPMENT_TEAM = XJF3S8KP2L; 659 | INFOPLIST_FILE = NovaTests/Info.plist; 660 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 661 | PRODUCT_BUNDLE_IDENTIFIER = jurkin.osx.NovaTests; 662 | PRODUCT_NAME = "$(TARGET_NAME)"; 663 | SWIFT_VERSION = 3.0; 664 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nova.app/Contents/MacOS/Nova"; 665 | }; 666 | name = Debug; 667 | }; 668 | 0A2D3E511F5AC429002510BA /* Release */ = { 669 | isa = XCBuildConfiguration; 670 | buildSettings = { 671 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 672 | BUNDLE_LOADER = "$(TEST_HOST)"; 673 | COMBINE_HIDPI_IMAGES = YES; 674 | DEVELOPMENT_TEAM = XJF3S8KP2L; 675 | INFOPLIST_FILE = NovaTests/Info.plist; 676 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 677 | PRODUCT_BUNDLE_IDENTIFIER = jurkin.osx.NovaTests; 678 | PRODUCT_NAME = "$(TARGET_NAME)"; 679 | SWIFT_VERSION = 3.0; 680 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nova.app/Contents/MacOS/Nova"; 681 | }; 682 | name = Release; 683 | }; 684 | /* End XCBuildConfiguration section */ 685 | 686 | /* Begin XCConfigurationList section */ 687 | 0A2D3E2D1F5AC429002510BA /* Build configuration list for PBXProject "Nova" */ = { 688 | isa = XCConfigurationList; 689 | buildConfigurations = ( 690 | 0A2D3E4A1F5AC429002510BA /* Debug */, 691 | 0A2D3E4B1F5AC429002510BA /* Release */, 692 | ); 693 | defaultConfigurationIsVisible = 0; 694 | defaultConfigurationName = Release; 695 | }; 696 | 0A2D3E4C1F5AC429002510BA /* Build configuration list for PBXNativeTarget "Nova" */ = { 697 | isa = XCConfigurationList; 698 | buildConfigurations = ( 699 | 0A2D3E4D1F5AC429002510BA /* Debug */, 700 | 0A2D3E4E1F5AC429002510BA /* Release */, 701 | ); 702 | defaultConfigurationIsVisible = 0; 703 | defaultConfigurationName = Release; 704 | }; 705 | 0A2D3E4F1F5AC429002510BA /* Build configuration list for PBXNativeTarget "NovaTests" */ = { 706 | isa = XCConfigurationList; 707 | buildConfigurations = ( 708 | 0A2D3E501F5AC429002510BA /* Debug */, 709 | 0A2D3E511F5AC429002510BA /* Release */, 710 | ); 711 | defaultConfigurationIsVisible = 0; 712 | defaultConfigurationName = Release; 713 | }; 714 | /* End XCConfigurationList section */ 715 | }; 716 | rootObject = 0A2D3E2A1F5AC429002510BA /* Project object */; 717 | } 718 | --------------------------------------------------------------------------------