├── LaunchAtLoginHelper
├── .gitignore
├── LaunchAtLoginHelper
│ ├── LaunchAtLoginHelper.entitlements
│ ├── LLHAppDelegate.h
│ ├── main.m
│ ├── LaunchAtLoginHelper-InfoBase.plist
│ ├── LaunchAtLoginHelper-Info.plist
│ ├── LLHAppDelegate.m
│ └── MainMenu.xib
├── LaunchAtLoginHelper.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── project.pbxproj
├── setup.py
├── LICENSE
└── readme.md
├── Support
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon-16x16.png
│ │ ├── icon-32x32.png
│ │ ├── icon-128x128.png
│ │ ├── icon-16x16@2x.png
│ │ ├── icon-256x256.png
│ │ ├── icon-32x32@2x.png
│ │ ├── icon-512x512.png
│ │ ├── icon-128x128@2x.png
│ │ ├── icon-256x256@2x.png
│ │ ├── icon-512x512@2x.png
│ │ └── Contents.json
│ ├── icon-facebook.imageset
│ │ ├── facebook.pdf
│ │ └── Contents.json
│ ├── icon-upvote.imageset
│ │ ├── icon-upvote.pdf
│ │ └── Contents.json
│ ├── icon-kitty.imageset
│ │ ├── icon-kitty@2x.png
│ │ └── Contents.json
│ ├── comment-icon.imageset
│ │ ├── icon-comment@2x.png
│ │ └── Contents.json
│ ├── icon-reload.imageset
│ │ ├── icon-reload@2x.png
│ │ └── Contents.json
│ ├── icon-settings.imageset
│ │ ├── icon-settings.png
│ │ └── Contents.json
│ ├── icon-twitter.imageset
│ │ ├── icon-twitter-1.pdf
│ │ └── Contents.json
│ ├── placeholder.imageset
│ │ ├── placeholder@2x.png
│ │ └── Contents.json
│ ├── icon-external-link.imageset
│ │ ├── External Link.pdf
│ │ └── Contents.json
│ ├── Icon-product-hunt.imageset
│ │ ├── Icon-product-hunt@2x.png
│ │ └── Contents.json
│ ├── icon-facebook-hovered.imageset
│ │ ├── facebook_hover.pdf
│ │ └── Contents.json
│ ├── icon-twitter-hovered.imageset
│ │ ├── icon-twitter-hovered.pdf
│ │ └── Contents.json
│ └── StatusBarButtonImage.imageset
│ │ ├── StatusBarButtonImage@2x.png
│ │ └── Contents.json
├── ProductHunt.entitlements
├── ProductHunt-Bridging-Header.h
└── Info.plist
├── Source
├── Components
│ ├── KPCScaleToFillNSImageView.h
│ ├── PHSettings
│ │ ├── PHPreferencesWindowControllerProtocol.swift
│ │ └── PHPreferencesWindowController.swift
│ └── KPCScaleToFillNSImageView.m
├── Tests
│ └── UnitTests
│ │ ├── Support
│ │ ├── UnitTests-Bridging-Header.h
│ │ ├── XCTestCase+ProductHunt.swift
│ │ ├── PHTestCase.swift
│ │ ├── Info.plist
│ │ ├── PHFakeFactory.swift
│ │ └── PHFakeEndpoint.swift
│ │ ├── PHSettingsModuleTests.swift
│ │ ├── PHTokenTests.swift
│ │ ├── PHSeenPostsModuleTests.swift
│ │ ├── PHDateFormatterTests.swift
│ │ ├── PHPostsModuleTests.swift
│ │ ├── PHPostSorterTests.swift
│ │ ├── PHPostViewModelTests.swift
│ │ └── PHPostDataSourceTests.swift
├── PHAppState.swift
├── Config
│ ├── PHConfiguration.h
│ ├── PHConfiguration.m
│ ├── ConfigDebug.xcconfig
│ ├── ConfigRelease.xcconfig
│ └── Keys-Example.xcconfig
├── Models
│ ├── PHShareMessage.swift
│ ├── Domain Objects
│ │ ├── PHSection.swift
│ │ ├── PHToken.swift
│ │ ├── PHTokenFactory.swift
│ │ ├── PHPost.swift
│ │ └── PHPostFactory.swift
│ ├── PHCallbacks.swift
│ ├── PHAnalitycsAPI.swift
│ ├── Credentials.swift
│ ├── ViewModels
│ │ └── PHPostViewModel.swift
│ ├── PHStatusBarUpdater.swift
│ ├── PHPostSorter.swift
│ ├── PHBundle.swift
│ ├── PHAnalitycsAPIEndpoint.swift
│ ├── API
│ │ ├── PHAPI.swift
│ │ └── PHAPIEndpoint.swift
│ ├── PHPostsDataSource.swift
│ ├── PHAnalitycs.swift
│ ├── PHDateFormatter.swift
│ └── PHDefaults.swift
├── Actions
│ ├── PHStartAtLoginAction.swift
│ ├── PHFirstLaunchAction.swift
│ ├── PHOpenURLAction.swift
│ ├── PHOpenSettingsAction.swift
│ ├── PHOpenProductHuntAction.swift
│ ├── PHScheduleAsSeenAction.swift
│ ├── PHPopoverAction.swift
│ ├── PHOpenSettingsMenuAction.swift
│ └── PHShareAction.swift
├── Categories
│ ├── NSButton+ProductHunt.swift
│ ├── NSImage+ProductHunt.swift
│ ├── NSError+ProductHunt.swift
│ └── NSColor+ProductHunt.swift
├── PHAnalitycsModule.swift
├── Views
│ ├── PHSeenView.swift
│ ├── PHScrollView.swift
│ ├── PHSectionCell.swift
│ ├── PHLoadingView.swift
│ ├── PHButton.swift
│ └── PHPostCell.swift
├── PHTokenModule.swift
├── PHAnalitycsOperation.swift
├── PHAppReducer.swift
├── PHMarkAsSeenOperation.swift
├── PHAppStatePosts.swift
├── PHMiddleware.swift
├── PHOpenPostOperation.swift
├── PHSeenPostsModule.swift
├── PHPostsModule.swift
├── PHSettingsModule.swift
├── PHAPIOperation.swift
├── PHLoadPostOperation.swift
├── Controllers
│ ├── PHAdvancedSettingsViewController.swift
│ ├── PHGeneralSettingsViewController.swift
│ ├── PHPostListViewController.swift
│ ├── PHAdvancedSettingsViewController.xib
│ └── PHGeneralSettingsViewController.xib
└── AppDelegate.swift
├── Product Hunt.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ ├── Release.xcscheme
│ └── Debug.xcscheme
├── Product Hunt.xcworkspace
└── contents.xcworkspacedata
├── .travis.yml
├── .gitignore
├── LaunchAtLogin
├── LLManager.h
└── LLManager.m
├── Podfile
├── LICENSE
├── Podfile.lock
└── README.md
/LaunchAtLoginHelper/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | xcuserdata
--------------------------------------------------------------------------------
/Support/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-16x16.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-32x32.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-128x128.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-16x16@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-256x256.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-32x32@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-512x512.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-facebook.imageset/facebook.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-facebook.imageset/facebook.pdf
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-upvote.imageset/icon-upvote.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-upvote.imageset/icon-upvote.pdf
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-128x128@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-256x256@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/icon-512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/AppIcon.appiconset/icon-512x512@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-kitty.imageset/icon-kitty@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-kitty.imageset/icon-kitty@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/comment-icon.imageset/icon-comment@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/comment-icon.imageset/icon-comment@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-reload.imageset/icon-reload@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-reload.imageset/icon-reload@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-settings.imageset/icon-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-settings.imageset/icon-settings.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-twitter.imageset/icon-twitter-1.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-twitter.imageset/icon-twitter-1.pdf
--------------------------------------------------------------------------------
/Support/Assets.xcassets/placeholder.imageset/placeholder@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/placeholder.imageset/placeholder@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-external-link.imageset/External Link.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-external-link.imageset/External Link.pdf
--------------------------------------------------------------------------------
/Support/Assets.xcassets/Icon-product-hunt.imageset/Icon-product-hunt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/Icon-product-hunt.imageset/Icon-product-hunt@2x.png
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-facebook-hovered.imageset/facebook_hover.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-facebook-hovered.imageset/facebook_hover.pdf
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-twitter-hovered.imageset/icon-twitter-hovered.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/icon-twitter-hovered.imageset/icon-twitter-hovered.pdf
--------------------------------------------------------------------------------
/Support/Assets.xcassets/StatusBarButtonImage.imageset/StatusBarButtonImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/producthunt/producthunt-osx/HEAD/Support/Assets.xcassets/StatusBarButtonImage.imageset/StatusBarButtonImage@2x.png
--------------------------------------------------------------------------------
/Support/ProductHunt.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Support/ProductHunt-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import "LLManager.h"
6 | #import "PHConfiguration.h"
7 | #import "KPCScaleToFillNSImageView.h"
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-facebook.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "filename" : "facebook.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper/LaunchAtLoginHelper.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-twitter.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "filename" : "icon-twitter-1.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-external-link.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "filename" : "External Link.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/Source/Components/KPCScaleToFillNSImageView.h:
--------------------------------------------------------------------------------
1 | //
2 | // KPCScaleToFillNSImageView.h
3 | //
4 | // Created by onekiloparsec on 4/5/14.
5 | // MIT Licence
6 | //
7 |
8 | #import
9 |
10 | @interface KPCScaleToFillNSImageView : NSImageView
11 | @end
12 |
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-facebook-hovered.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "filename" : "facebook_hover.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-twitter-hovered.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "filename" : "icon-twitter-hovered.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/Product Hunt.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Product Hunt.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/Support/UnitTests-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // UnitTests-Bridging-Header.h
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/29/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | #import "LLManager.h"
10 | #import "PHConfiguration.h"
11 | #import "KPCScaleToFillNSImageView.h"
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHSettingsModuleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSettingsModuleTests.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/4/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import ReSwift
11 |
12 | class PHSettingsModuleTests: PHTestCase {
13 | // TODO:
14 | }
15 |
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-kitty.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "mac",
9 | "filename" : "icon-kitty@2x.png",
10 | "scale" : "2x"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-upvote.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "filename" : "icon-upvote.pdf",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Support/Assets.xcassets/comment-icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "mac",
9 | "filename" : "icon-comment@2x.png",
10 | "scale" : "2x"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-reload.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "mac",
9 | "filename" : "icon-reload@2x.png",
10 | "scale" : "2x"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Support/Assets.xcassets/placeholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "mac",
9 | "filename" : "placeholder@2x.png",
10 | "scale" : "2x"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Support/Assets.xcassets/Icon-product-hunt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "mac",
9 | "filename" : "Icon-product-hunt@2x.png",
10 | "scale" : "2x"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Source/PHAppState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAppState.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/27/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import ReSwift
11 |
12 | struct PHAppState: StateType {
13 | var settings: PHSettings
14 | var posts: PHAppStatePosts
15 | var seenPosts: PHSeenPosts
16 | var token: PHToken
17 | }
--------------------------------------------------------------------------------
/Source/Config/PHConfiguration.h:
--------------------------------------------------------------------------------
1 | //
2 | // PHConfiguration.h
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/11/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | extern NSString * const kPHAppID;
12 | extern NSString * const kPHAppSecret;
13 | extern NSString * const kPHFeedUrl;
14 | extern NSString * const kSegmentKey;
15 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper/LLHAppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // LLHAppDelegate.h
3 | // LaunchAtLoginHelper
4 | //
5 | // Created by David Keegan on 4/20/12.
6 | // Copyright (c) 2012 David Keegan.
7 | // Some rights reserved:
8 | //
9 |
10 | #import
11 |
12 | @interface LLHAppDelegate : NSObject
13 |
14 | @end
15 |
--------------------------------------------------------------------------------
/Source/Models/PHShareMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHShareMessage.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 4/6/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHShareMessage {
12 |
13 | class func message(fromPost post: PHPost) -> String {
14 | return "\(post.title): \(post.tagline) \(post.discussionUrl)"
15 | }
16 | }
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // LaunchAtLoginHelper
4 | //
5 | // Created by David Keegan on 4/20/12.
6 | // Copyright (c) 2012 David Keegan.
7 | // Some rights reserved:
8 | //
9 |
10 | #import
11 |
12 | int main(int argc, char *argv[]){
13 | return NSApplicationMain(argc, (const char **)argv);
14 | }
15 |
--------------------------------------------------------------------------------
/Source/Actions/PHStartAtLoginAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHStartAtLoginAction.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/22/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ServiceManagement
11 |
12 | class PHStartAtLoginAction {
13 |
14 | class func perform(_ startAtLogin: Bool) {
15 | LLManager.setLaunchAtLogin(startAtLogin)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Support/Assets.xcassets/icon-settings.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "mac",
9 | "filename" : "icon-settings.png",
10 | "scale" : "2x"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | },
17 | "properties" : {
18 | "template-rendering-intent" : "template"
19 | }
20 | }
--------------------------------------------------------------------------------
/Source/Categories/NSButton+ProductHunt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSButton+ProductHunt.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/18/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | extension NSButton {
12 |
13 | var boolState: Bool {
14 | return state == 1 ? true : false
15 | }
16 |
17 | func setState(forBool bool: Bool) {
18 | state = bool ? 1 : 0
19 | }
20 | }
--------------------------------------------------------------------------------
/Source/Config/PHConfiguration.m:
--------------------------------------------------------------------------------
1 | //
2 | // PHConfiguration.m
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/11/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | #import "PHConfiguration.h"
10 |
11 | NSString *const kPHAppID = PRODUCT_HUNT_APP_ID;
12 | NSString *const kPHAppSecret = PRODUCT_HUNT_APP_SECRET;
13 | NSString *const kPHFeedUrl = PRODUCT_HUNT_FEED_URL;
14 | NSString *const kSegmentKey = SEGMENT_WRITE_KEY;
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: Swift
2 |
3 | xcode_project: Product\ Hunt.xcodeproj
4 | xcode_scheme: Debug
5 | osx_image: xcode7.3
6 | os: osx
7 |
8 | before_install:
9 |
10 | - export LANG=en_US.UTF-8
11 | - gem install cocoapods --quiet
12 | - cp Source/Config/Keys-Example.xcconfig Source/Config/Keys.xcconfig
13 |
14 | script:
15 |
16 | - xctool -workspace Product\ Hunt.xcworkspace -scheme Debug -sdk macosx clean test CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
17 |
--------------------------------------------------------------------------------
/Source/PHAnalitycsModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAnalitycsModule.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import ReSwift
10 |
11 | struct PHTrackPostAction: Action {
12 | var post: PHPost
13 | }
14 |
15 | struct PHTrackPostShare: Action {
16 | var post: PHPost
17 | var medium: String
18 | }
19 |
20 | struct PHTrackVisit: Action {
21 | var page: String
22 | }
--------------------------------------------------------------------------------
/Source/Models/Domain Objects/PHSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSectionNew.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/29/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct PHSection {
12 |
13 | static func section(_ posts: [PHPost]) -> PHSection {
14 | return PHSection(day: posts.first!.day, posts: posts)
15 | }
16 |
17 | var day: String
18 | var posts: [PHPost]
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Source/Config/ConfigDebug.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigDebug.xcconfig
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | #include "Keys.xcconfig"
10 |
11 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) PRODUCT_HUNT_APP_ID='$(PRODUCT_HUNT_APP_ID)' PRODUCT_HUNT_APP_SECRET='$(PRODUCT_HUNT_APP_SECRET)' PRODUCT_HUNT_FEED_URL='$(PRODUCT_HUNT_FEED_URL)' SEGMENT_WRITE_KEY='$(SEGMENT_DEVELOPMENT_WRITE_KEY)'
12 |
--------------------------------------------------------------------------------
/Source/Config/ConfigRelease.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigRelease.xcconfig
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | #include "Keys.xcconfig"
10 |
11 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) PRODUCT_HUNT_APP_ID='$(PRODUCT_HUNT_APP_ID)' PRODUCT_HUNT_APP_SECRET='$(PRODUCT_HUNT_APP_SECRET)' PRODUCT_HUNT_FEED_URL='$(PRODUCT_HUNT_FEED_URL)' SEGMENT_WRITE_KEY='$(SEGMENT_PRODUCTION_WRITE_KEY)'
12 |
--------------------------------------------------------------------------------
/Source/Models/Domain Objects/PHToken.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHToken.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct PHToken {
12 | var accessToken: String
13 |
14 | var isValid: Bool {
15 | return !accessToken.isEmpty
16 | }
17 |
18 | func description() -> [String: AnyObject] {
19 | return ["access_token" : accessToken as AnyObject]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Support/Assets.xcassets/StatusBarButtonImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac"
5 | },
6 | {
7 | "idiom" : "mac",
8 | "scale" : "1x"
9 | },
10 | {
11 | "idiom" : "mac",
12 | "filename" : "StatusBarButtonImage@2x.png",
13 | "scale" : "2x"
14 | }
15 | ],
16 | "info" : {
17 | "version" : 1,
18 | "author" : "xcode"
19 | },
20 | "properties" : {
21 | "template-rendering-intent" : "template"
22 | }
23 | }
--------------------------------------------------------------------------------
/Source/Config/Keys-Example.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // Keys-Example.xcconfig
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/11/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | // API Keys generated from https://www.producthunt.com/v1/oauth/applications
10 |
11 | PRODUCT_HUNT_APP_ID = @"APP_ID_KEY";
12 | PRODUCT_HUNT_APP_SECRET = @"APP_SECRET_KEY";
13 |
14 | PRODUCT_HUNT_FEED_URL = @"PRODUCT_HUNT_FEED_URL";
15 |
16 | SEGMENT_PRODUCTION_WRITE_KEY = @"SEGMENT_PRODUCTION_WRITE_KEY";
17 | SEGMENT_DEVELOPMENT_WRITE_KEY = @"SEGMENT_DEVELOPMENT_WRITE_KEY";
18 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/Support/XCTestCase+ProductHunt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProductHunt.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | extension XCTestCase {
12 |
13 | func ph_expectation(_ description: String, fulfill: (_ expectation: XCTestExpectation) -> Void) {
14 | let expectation = self.expectation(description: description)
15 |
16 | fulfill(expectation)
17 |
18 | waitForExpectations(timeout: 5, handler: nil)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Source/Models/Domain Objects/PHTokenFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHTokenFactory.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension PHToken {
12 |
13 | static func token(fromDictionary dictionary: [String: Any]?) -> PHToken? {
14 | guard let dictionary = dictionary, let accessToken = dictionary["access_token"] as? String else {
15 | return nil
16 | }
17 |
18 | return PHToken(accessToken: accessToken)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Source/Models/PHCallbacks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHCallbacks.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/30/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias PHVoidCallback = (Void) -> Void
12 | typealias PHAPIErrorClosure = (_ error: NSError) -> ()
13 | typealias PHAPIOperationClosure = (_ api: PHAPI,_ errorClosure: @escaping PHAPIErrorClosure) -> ()
14 | typealias PHAPITokenCompletion = ((_ token: PHToken?, _ error: NSError?) -> ())
15 | typealias PHAPIPostCompletion = ((_ posts: [PHPost], _ error: NSError?) -> ())
16 |
--------------------------------------------------------------------------------
/Source/Views/PHSeenView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSeenView.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/17/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHSeenView: NSView {
12 |
13 | override func awakeFromNib() {
14 | super.awakeFromNib()
15 |
16 | wantsLayer = true
17 | layer?.backgroundColor = NSColor.ph_orangeColor().cgColor
18 | }
19 |
20 | override func draw(_ dirtyRect: NSRect) {
21 | super.draw(dirtyRect)
22 |
23 | layer?.cornerRadius = dirtyRect.size.height/2
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Source/Actions/PHFirstLaunchAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHFirstLaunchAction.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/22/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHFirstLaunchAction {
12 |
13 | class func perform(_ completion: PHVoidCallback) {
14 | let defaults = UserDefaults.standard
15 |
16 | if !defaults.bool(forKey: "alreadyLaunched") {
17 |
18 | completion()
19 |
20 | defaults.set(true, forKey: "alreadyLaunched")
21 | defaults.synchronize()
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/Components/PHSettings/PHPreferencesWindowControllerProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHTestSettingsWindowControllerProtocol.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/22/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | @objc protocol PHPreferencesWindowControllerProtocol {
12 |
13 | func preferencesIdentifier() -> String
14 |
15 | func preferencesTitle() -> String
16 |
17 | func preferencesIcon() -> NSImage
18 |
19 | @objc optional func firstResponder() -> NSResponder
20 |
21 | @objc optional func preferencesToolTip() -> String
22 | }
23 |
--------------------------------------------------------------------------------
/Source/PHTokenModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHTokenModule.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/3/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | struct PHTokenGetAction: Action {
13 | var token: PHToken
14 | }
15 |
16 | func tokenReducer(_ action: Action, state: PHToken?) -> PHToken {
17 | let state = state ?? PHToken(accessToken: "")
18 |
19 | switch action {
20 | case let action as PHTokenGetAction:
21 | return action.token
22 |
23 | default:
24 | return state
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### Objective-C ###
4 | # Xcode
5 | #
6 | build/
7 | *.pbxuser
8 | !default.pbxuser
9 | *.mode1v3
10 | !default.mode1v3
11 | *.mode2v3
12 | !default.mode2v3
13 | *.perspectivev3
14 | !default.perspectivev3
15 | xcuserdata
16 | *.xccheckout
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 | *.xcuserstate
22 |
23 | Pods/*
24 |
25 | # fastlane
26 | fastlane/report.xml
27 | fastlane/screenshots
28 | fastlane/test_output
29 | ProductHunt.app.dSYM.zip
30 | /*.mobileprovision
31 |
32 | # FileSystem
33 | .DS_Store
34 |
35 | UITests/.DS_Store
36 |
37 | Source/Config/Keys.xcconfig
38 |
--------------------------------------------------------------------------------
/Source/Views/PHScrollView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHTableView.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/17/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHScrollView: NSScrollView {
12 |
13 | override func draw(_ dirtyRect: NSRect) {
14 | super.draw(dirtyRect)
15 |
16 | let layer = CALayer()
17 | layer.borderColor = NSColor.windowBackgroundColor.cgColor
18 | layer.borderWidth = 1
19 | layer.frame = NSRect(x: 0, y: dirtyRect.size.height - 1, width: dirtyRect.size.width, height: 1)
20 |
21 | self.layer?.addSublayer(layer)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Source/Categories/NSImage+ProductHunt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSImage+ProductHunt.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 4/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | extension NSImage {
12 |
13 | func tintedImageWithColor(_ color: NSColor) -> NSImage {
14 | let tinted = self.copy() as! NSImage
15 | tinted.lockFocus()
16 | color.set()
17 |
18 | let imageRect = NSRect(origin: NSZeroPoint, size: self.size)
19 | NSRectFillUsingOperation(imageRect, NSCompositingOperation.sourceAtop)
20 |
21 | tinted.unlockFocus()
22 | return tinted
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/PHAnalitycsOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAnalitycsOperation.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHAnalitycsOperation {
12 |
13 | class func performTrack(_ post: PHPost) {
14 | store.dispatch( PHTrackPostAction(post: post) )
15 | }
16 |
17 | class func performTrackShare(_ post: PHPost, medium: String) {
18 | store.dispatch( PHTrackPostShare(post: post, medium: medium) )
19 | }
20 |
21 | class func performTrackVisit(_ page: String) {
22 | store.dispatch( PHTrackVisit(page: page) )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/Support/PHTestCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHtestCase.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/18/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import DateTools
11 | import SwiftyTimer
12 |
13 | class PHTestCase: XCTestCase {
14 |
15 | var fake = PHFakeFactory.sharedInstance
16 |
17 | var endpoint = PHAPIFakeEndpoint(token: PHFakeFactory.sharedInstance.token())
18 |
19 | override func setUp() {
20 | super.setUp()
21 |
22 | PHAPI.sharedInstance.endpoint = endpoint
23 | }
24 |
25 | override func tearDown() {
26 | super.tearDown()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Source/Actions/PHOpenURLAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHOpenURLAction.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/16/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHOpenURLAction {
12 |
13 | class func perform(withPath path: String, closeAfterLaunch: Bool = false) {
14 | perform(withUrl: URL(string: path)!, closeAfterLaunch: closeAfterLaunch)
15 | }
16 |
17 | class func perform(withUrl url: URL, closeAfterLaunch: Bool = false) {
18 | let handle = NSWorkspace.shared().open(url)
19 |
20 | if handle && closeAfterLaunch {
21 | PHPopoverAction.close()
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/Categories/NSError+ProductHunt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSError+ProductHunt.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/29/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension NSError {
12 |
13 | class func parseError(_ error: NSError?) -> NSError? {
14 | if let error = error, (error.userInfo[NSLocalizedDescriptionKey] as? String)?.contains("401") != nil {
15 | return unauthorizedError()
16 | }
17 |
18 | return error
19 | }
20 |
21 | class func unauthorizedError() -> NSError {
22 | return NSError(domain: "com.producthunt", code: 401, userInfo: nil)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Source/PHAppReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAppReducer.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/27/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | struct PHAppReducer: Reducer {
13 |
14 | func handleAction(action: Action, state: PHAppState?) -> PHAppState {
15 | return PHAppState(
16 | settings: settingsReducer(action, state: state?.settings),
17 | posts: postsReducer(action, state: state?.posts),
18 | seenPosts: seenPostsReducer(action, state: state?.seenPosts),
19 | token: tokenReducer(action, state: state?.token)
20 | )
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/LaunchAtLogin/LLManager.h:
--------------------------------------------------------------------------------
1 | //
2 | // LLManager.h
3 | // LaunchAtLogin
4 | //
5 | // Created by David Keegan on 4/20/12.
6 | // Copyright (c) 2012 David Keegan.
7 | // Copyright (c) 2014 Jan Weiß.
8 | // Some rights reserved:
9 | //
10 |
11 | #import
12 |
13 | extern NSString * const LLManagerSetLaunchAtLoginFailedNotification;
14 |
15 | @interface LLManager : NSObject
16 |
17 | + (BOOL)launchAtLogin;
18 | + (void)setLaunchAtLogin:(BOOL)value;
19 | + (void)setLaunchAtLogin:(BOOL)value notifyOnFailure:(BOOL)wantFailureNotification;
20 |
21 | @property (assign) BOOL launchAtLogin;
22 | @property (assign) BOOL notifyIfSetLaunchAtLoginFailed;
23 |
24 | @end
25 |
--------------------------------------------------------------------------------
/Source/Actions/PHOpenSettingsAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHShowSettingsAction.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/21/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHOpenSettingsAction {
12 |
13 | class func perform() {
14 | let delegate = NSApplication.shared().delegate as! AppDelegate
15 |
16 | delegate.settingsWindow = PHPreferencesWindowController()
17 | delegate.settingsWindow.viewControllers = [ PHGeneralSettingsViewController(nibName: "PHGeneralSettingsViewController", bundle: nil)!, PHAdvancedSettingsViewController(nibName: "PHAdvancedSettingsViewController", bundle: nil)! ]
18 |
19 | delegate.settingsWindow.showPreferencesWindow()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Source/Actions/PHOpenProductHuntAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHOpenProductHuntAction.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHOpenProductHuntAction: NSObject {
12 |
13 | class func performWithFAQLink() {
14 | PHOpenURLAction.perform(withPath: "https://www.producthunt.com/faq", closeAfterLaunch: true)
15 | }
16 |
17 | class func performWithAboutLink() {
18 | PHOpenURLAction.perform(withPath: "https://www.producthunt.com/about", closeAfterLaunch: true)
19 | }
20 |
21 | class func performWithAppsLink() {
22 | PHOpenURLAction.perform(withPath: "https://www.producthunt.com/apps", closeAfterLaunch: true)
23 | }
24 | }
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | use_frameworks!
2 | inhibit_all_warnings!
3 |
4 | target 'ProductHunt' do
5 | pod 'Kingfisher', '~> 3.0'
6 | pod 'AFNetworking', '~> 3.0'
7 | pod 'ISO8601', '~> 0.5'
8 | pod 'DateTools'
9 | pod 'SwiftyTimer'
10 | pod 'Sparkle'
11 | pod 'ReSwift'
12 |
13 | target 'ProductHuntTests' do
14 |
15 | end
16 |
17 | end
18 |
19 | post_install do |installer|
20 | installer.pods_project.targets.each do |target|
21 | target.build_configurations.each do |config|
22 | config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
23 | config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
24 | config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
25 | config.build_settings['SWIFT_VERSION'] = '3.0'
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/Source/Models/PHAnalitycsAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAnalitycsAPI.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ISO8601
11 |
12 | class PHAnalitycsAPI {
13 |
14 | var endpoint = PHAnalitycsAPIEndpoint(key: kSegmentKey)
15 |
16 | func track(_ properties: PHAnalitycsProperties) {
17 | endpoint.post("track", parameters: properties)
18 | }
19 |
20 | // Use `page` instead of `screen`
21 | func visit(_ properties: PHAnalitycsProperties) {
22 | endpoint.post("page", parameters: properties)
23 | }
24 |
25 | func identify(_ properties: PHAnalitycsProperties) {
26 | endpoint.post("identify", parameters: properties)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Source/PHMarkAsSeenOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHMarkAsSeenOperation.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/3/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | class PHMarkAsSeenOperation {
13 |
14 | class func perform(_ post: PHPost) {
15 | perform(store, posts: [post])
16 | }
17 |
18 | class func perform(_ posts: [PHPost]) {
19 | perform(store, posts: posts)
20 | }
21 |
22 | class func perform(_ store: Store, posts: [PHPost]) {
23 | let filters: [PHPostFilter] = [.seen(false), .votes(store.state.settings.filterCount)]
24 | store.dispatch( PHMarkPostsAsSeenAction(posts: PHPostSorter.filter(store, posts: posts, by: filters) ) )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHTokenTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHTokenTests.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class PHTokenTests: PHTestCase {
12 |
13 | func testTokenFactoryRetunsNilIfInvalidAccessToken() {
14 | let data = ["access_token": NSNull()]
15 | XCTAssertNil(PHToken.token(fromDictionary: data))
16 | }
17 |
18 | func testThatReturnsFalseIfTokenIsExpired() {
19 | let token = PHToken(accessToken: "")
20 |
21 | XCTAssertFalse(token.isValid)
22 | }
23 |
24 | func testThatReturnsTrueIfTokenIsExpired() {
25 | let token = PHToken(accessToken: "3asdsfgdsv" )
26 |
27 | XCTAssertTrue(token.isValid)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Source/PHAppStatePosts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAppStatePosts.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/3/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | struct PHAppStatePosts {
13 |
14 | var sections: [PHSection]
15 |
16 | var today: Int {
17 | return 0
18 | }
19 |
20 | var nextDay: Int {
21 | guard let section = sections.last else {
22 | return 0
23 | }
24 |
25 | return PHDateFormatter.daysAgo(section.day) + 1
26 | }
27 |
28 | var todayPosts: [PHPost]? {
29 | guard let section = sections.first else {
30 | return nil
31 | }
32 |
33 | return section.posts
34 | }
35 |
36 | var lastUpdated: Date
37 | }
38 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/setup.py:
--------------------------------------------------------------------------------
1 | import sys, os
2 | import plistlib
3 |
4 | urlScheme = sys.argv[1]
5 | bundleIdentifier = sys.argv[2]
6 |
7 | directory = os.path.dirname(os.path.abspath(__file__))
8 |
9 | stringsOutput = os.path.join(directory, 'LLStrings.h')
10 | infoPlistOutput = os.path.join(directory, 'LaunchAtLoginHelper/LaunchAtLoginHelper-Info.plist')
11 | infoPlist = plistlib.readPlist(os.path.join(directory, 'LaunchAtLoginHelper/LaunchAtLoginHelper-InfoBase.plist'))
12 |
13 | with open(stringsOutput, 'w') as strings:
14 | strings.write("""// strings used by LLManager and LaunchAtLoginHelper
15 | //
16 |
17 | #define LLURLScheme @"%(urlScheme)s"
18 | #define LLHelperBundleIdentifier @"%(bundleIdentifier)s"
19 | """%locals())
20 |
21 | infoPlist['CFBundleIdentifier'] = bundleIdentifier
22 | plistlib.writePlist(infoPlist, infoPlistOutput)
23 |
--------------------------------------------------------------------------------
/Source/Views/PHSectionCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSectionCell.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/16/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHSectionCell: NSTableCellView {
12 |
13 | class func view(_ tableView: NSTableView, owner: AnyObject?, subject: AnyObject?) -> NSView? {
14 | guard let section = subject as? String else {
15 | return nil
16 | }
17 |
18 | let view = tableView.make(withIdentifier: "sectionCellIdentifier", owner: owner) as! PHSectionCell
19 | view.textField?.stringValue = section
20 | return view
21 | }
22 |
23 | override func awakeFromNib() {
24 | super.awakeFromNib()
25 |
26 | wantsLayer = true
27 | layer?.backgroundColor = NSColor.white.cgColor
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Source/Models/Credentials.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Credentials.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | open class Credentials {
12 |
13 | /** Returns this string encoded as Base64. */
14 | static func base64(_ string: String) -> String {
15 | let utf8str = string.data(using: String.Encoding.utf8, allowLossyConversion: false)!
16 | return utf8str.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength64Characters)
17 | }
18 |
19 | /** Returns an auth credential for the Basic scheme. Exposed for testing. */
20 | static func basic(_ username: String, password: String) -> String {
21 | return String(format: "Basic %@", base64(String(format: "%@:%@", username, password)))
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/Support/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 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Source/Actions/PHScheduleAsSeenAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSheduleAsSeenAction.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHScheduleAsSeenAction {
12 |
13 | fileprivate class var delegate: AppDelegate {
14 | return NSApplication.shared().delegate as! AppDelegate
15 | }
16 |
17 | class func performSchedule() {
18 | performCancel()
19 |
20 | delegate.countdownToMarkAsSeen = Timer.after(30.seconds, {
21 | guard let posts = store.state.posts.todayPosts else {
22 | return
23 | }
24 |
25 | PHMarkAsSeenOperation.perform( posts )
26 | })
27 | }
28 |
29 | class func performCancel() {
30 | delegate.countdownToMarkAsSeen?.invalidate()
31 | delegate.countdownToMarkAsSeen = nil
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Source/PHMiddleware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAPICallMiddleware.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/4/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | let PHTrackingMiddleware: Middleware = { dispatch, getState in
13 | return { next in
14 | return { action in
15 |
16 | if let action = action as? PHTrackPostAction {
17 | PHAnalitycs.sharedInstance.trackClickPost(action.post.id)
18 | }
19 |
20 | if let action = action as? PHTrackPostShare {
21 | PHAnalitycs.sharedInstance.trackShare("post", subjectId: action.post.id, medium: action.medium)
22 | }
23 |
24 | if let action = action as? PHTrackVisit {
25 | PHAnalitycs.sharedInstance.trackVisit(action.page)
26 | }
27 |
28 | return next(action)
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Source/PHOpenPostOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHOpenPostOperation.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/4/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHOpenPostOperation {
12 |
13 | fileprivate class var openExternalLink: Bool {
14 | var openExternalLink = false
15 |
16 | if let event = NSApp.currentEvent, event.modifierFlags.contains(.command) && event.modifierFlags.contains(.option) {
17 | openExternalLink = true
18 | }
19 |
20 | return openExternalLink
21 | }
22 |
23 | class func perform(withPost post: PHPost) {
24 | let url = openExternalLink ? post.redirectUrl : post.discussionUrl
25 |
26 | PHOpenURLAction.perform(withUrl: url, closeAfterLaunch: true)
27 |
28 | PHMarkAsSeenOperation.perform(post)
29 |
30 | PHAnalitycsOperation.performTrack(post)
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Source/Categories/NSColor+ProductHunt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSColor+ProductHunt.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/24/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | extension NSColor {
12 |
13 | class func ph_whiteColor() -> NSColor {
14 | return NSColor.white
15 | }
16 |
17 | class func ph_highlightColor() -> NSColor {
18 | return NSColor(calibratedRed: 249/255, green: 249/255, blue: 249/255, alpha: 1)
19 | }
20 |
21 | class func ph_orangeColor() -> NSColor {
22 | return NSColor(calibratedRed: 228/255, green: 81/255, blue: 39/255, alpha: 1)
23 | }
24 |
25 | class func ph_grayColor() -> NSColor {
26 | return NSColor(calibratedRed: 153/255, green: 153/255, blue: 153/255, alpha: 1)
27 | }
28 |
29 | class func ph_lightGrayColor() -> NSColor {
30 | return NSColor(calibratedRed: 204/255, green: 204/255, blue: 208/255, alpha: 1)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 David Keegan (http://davidkeegan.com)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Source/Actions/PHPopoverAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPopoverAction.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/28/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHPopoverAction {
12 |
13 | fileprivate class var appDelegate: AppDelegate {
14 | return NSApplication.shared().delegate as! AppDelegate
15 | }
16 |
17 | class func toggle() {
18 | if appDelegate.popover.isShown {
19 | close()
20 | } else {
21 | show()
22 | }
23 | }
24 |
25 | class func close() {
26 | if !appDelegate.popover.isShown {
27 | return
28 | }
29 |
30 | appDelegate.popover.close()
31 | }
32 |
33 | class func show() {
34 | NSRunningApplication.current().activate(options: NSApplicationActivationOptions.activateIgnoringOtherApps)
35 |
36 | guard let button = appDelegate.statusItem.button else {
37 | return
38 | }
39 |
40 | appDelegate.popover.show(relativeTo: button.frame, of: button, preferredEdge: .minY)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Product Hunt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Source/Models/ViewModels/PHPostViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostViewModel.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/18/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import ReSwift
10 |
11 | class PHPostViewModel {
12 |
13 | var isSeen: Bool {
14 | return store.state.seenPosts.isSeen(post)
15 | }
16 |
17 | var title: String {
18 | return post.title
19 | }
20 |
21 | var tagline: String {
22 | return post.tagline
23 | }
24 |
25 | var thumbnailUrl: URL {
26 | return post.thumbnailUrl as URL
27 | }
28 |
29 | var votesCount: String {
30 | return "\(post.votesCount)"
31 | }
32 |
33 | var commentsCount: String {
34 | return "\(post.commentsCount)"
35 | }
36 |
37 | var createdAt: String {
38 | return formatter.timeAgo(fromDateAsString: post.createdAt)
39 | }
40 |
41 | fileprivate var post: PHPost
42 | fileprivate var store: Store
43 | fileprivate var formatter = PHDateFormatter()
44 |
45 | init(withPost post: PHPost, store: Store) {
46 | self.post = post
47 | self.store = store
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Source/Actions/PHOpenSettingsMenuAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHShowSettingsMenuAction.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/17/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHOpenSettingsMenuAction {
12 |
13 | class func perform(_ sender: NSView) {
14 | let delegate = NSApplication.shared().delegate as! AppDelegate
15 |
16 | let menu = NSMenu()
17 |
18 | menu.addItem(NSMenuItem(title: "Apps", action: #selector(delegate.openAppsLink), keyEquivalent: ""))
19 | menu.addItem(NSMenuItem(title: "FAQ", action: #selector(delegate.openFAQLink), keyEquivalent: ""))
20 | menu.addItem(NSMenuItem(title: "About", action: #selector(delegate.openAboutLink), keyEquivalent: ""))
21 | menu.addItem(NSMenuItem.separator())
22 | menu.addItem(NSMenuItem(title: "Check for Updates", action: #selector(delegate.checkForUpdates), keyEquivalent: "u"))
23 | menu.addItem(NSMenuItem(title: "Settings", action: #selector(delegate.showSettings), keyEquivalent: "s"))
24 | menu.addItem(NSMenuItem.separator())
25 | menu.addItem(NSMenuItem(title: "Quit", action: #selector(delegate.quit), keyEquivalent: "q"))
26 |
27 | NSMenu.popUpContextMenu(menu, with: NSApp.currentEvent!, for: sender)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper/LaunchAtLoginHelper-InfoBase.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | com.InScopeApps.ShellTo.${PRODUCT_NAME:rfc1034identifier}
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | ${PRODUCT_NAME}
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSMinimumSystemVersion
26 | ${MACOSX_DEPLOYMENT_TARGET}
27 | LSUIElement
28 |
29 | NSHumanReadableCopyright
30 | Copyright © 2012 David Keegan. All rights reserved.
31 | NSMainNibFile
32 | MainMenu
33 | NSPrincipalClass
34 | NSApplication
35 |
36 |
37 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper/LaunchAtLoginHelper-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSBackgroundOnly
6 |
7 | CFBundleDevelopmentRegion
8 | en
9 | CFBundleExecutable
10 | ${EXECUTABLE_NAME}
11 | CFBundleIconFile
12 |
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | ${PRODUCT_NAME}
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | 1.0
23 | CFBundleSignature
24 | ????
25 | CFBundleVersion
26 | 1
27 | LSMinimumSystemVersion
28 | ${MACOSX_DEPLOYMENT_TARGET}
29 | LSUIElement
30 |
31 | NSHumanReadableCopyright
32 | Copyright © 2012 David Keegan. All rights reserved.
33 | NSMainNibFile
34 | MainMenu
35 | NSPrincipalClass
36 | NSApplication
37 |
38 |
39 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper/LLHAppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // LLHAppDelegate.m
3 | // LaunchAtLoginHelper
4 | //
5 | // Created by David Keegan on 4/20/12.
6 | // Copyright (c) 2012 David Keegan.
7 | // Some rights reserved:
8 | //
9 |
10 | #import "LLHAppDelegate.h"
11 |
12 | NSString * const LLURLScheme = @"producthunt";
13 |
14 | @implementation LLHAppDelegate
15 |
16 | - (void)applicationDidFinishLaunching:(NSNotification *)notification {
17 |
18 | // The scheme to launch the app
19 | NSString *scheme = [NSString stringWithFormat:@"%@://", LLURLScheme];
20 | NSURL *schemeURL = [NSURL URLWithString:scheme];
21 |
22 | // Get URL for app that responds to scheme
23 | NSURL *appURL = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:schemeURL];
24 |
25 | // Check if app exists
26 | if(appURL) {
27 | // App exists, run it
28 | [[NSWorkspace sharedWorkspace] openURL:schemeURL];
29 |
30 | // Call the app again this time with `launchedAtLogin` so it knows how it was launched
31 | NSString *schemeLaunchedAtLogin = [NSString stringWithFormat:@"%@://launchedAtLogin", LLURLScheme];
32 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:schemeLaunchedAtLogin]];
33 | }
34 |
35 | [NSApp terminate:self];
36 | }
37 |
38 | @end
39 |
--------------------------------------------------------------------------------
/Source/Models/PHStatusBarUpdater.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHStatusBarUpdater.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/30/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import ReSwift
11 |
12 | class PHStatusBarUpdater: StoreSubscriber {
13 |
14 | fileprivate var button: NSStatusBarButton?
15 | fileprivate var store: Store
16 |
17 | init(button: NSStatusBarButton?, store: Store) {
18 | self.button = button
19 | self.store = store
20 |
21 | store.subscribe(self)
22 | }
23 |
24 | deinit {
25 | store.unsubscribe(self)
26 | }
27 |
28 | func newState(state: PHAppState) {
29 | updateTitle()
30 | }
31 |
32 | fileprivate func updateTitle() {
33 | guard let button = button, let posts = store.state.posts.todayPosts else {
34 | return
35 | }
36 |
37 | let sortedPosts = PHPostSorter.filter(store, posts: posts, by: [.seen(false), .votes(store.state.settings.filterCount)])
38 |
39 | button.title = title(fromCount: sortedPosts.count)
40 | }
41 |
42 | fileprivate func title(fromCount count: Int) -> String {
43 | if !store.state.settings.showsCount {
44 | return ""
45 | }
46 |
47 | return count > 9 ? "9+" : (count == 0 ? "" : "\(count)")
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - AFNetworking (3.1.0):
3 | - AFNetworking/NSURLSession (= 3.1.0)
4 | - AFNetworking/Reachability (= 3.1.0)
5 | - AFNetworking/Security (= 3.1.0)
6 | - AFNetworking/Serialization (= 3.1.0)
7 | - AFNetworking/UIKit (= 3.1.0)
8 | - AFNetworking/NSURLSession (3.1.0):
9 | - AFNetworking/Reachability
10 | - AFNetworking/Security
11 | - AFNetworking/Serialization
12 | - AFNetworking/Reachability (3.1.0)
13 | - AFNetworking/Security (3.1.0)
14 | - AFNetworking/Serialization (3.1.0)
15 | - DateTools (1.7.0)
16 | - ISO8601 (0.6.0)
17 | - Kingfisher (3.2.2)
18 | - ReSwift (3.0.0)
19 | - Sparkle (1.14.0)
20 | - SwiftyTimer (2.0.0)
21 |
22 | DEPENDENCIES:
23 | - AFNetworking (~> 3.0)
24 | - DateTools
25 | - ISO8601 (~> 0.5)
26 | - Kingfisher (~> 3.0)
27 | - ReSwift
28 | - Sparkle
29 | - SwiftyTimer
30 |
31 | SPEC CHECKSUMS:
32 | AFNetworking: 5e0e199f73d8626b11e79750991f5d173d1f8b67
33 | DateTools: 53288ee8b905fdc75897a1e6b5cc0144b14cba60
34 | ISO8601: d3ea3ba9b752820cf92c6b47a9ee327e9f0e13fc
35 | Kingfisher: 821466474ad7ffc375a4aecadfa05068cfed67b3
36 | ReSwift: f0255f4b4f2dacd12c5d97d34d13fc5dad6d6371
37 | Sparkle: ccd95233b12a3e3d4eeb55ff01dd4c8bb8188b07
38 | SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf
39 |
40 | PODFILE CHECKSUM: 4a2d06f24ccf54d50b6032a7d9b83025c887d7ad
41 |
42 | COCOAPODS: 1.1.1
43 |
--------------------------------------------------------------------------------
/Source/Models/PHPostSorter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostSorter.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/19/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | enum PHPostFilter {
13 | case seen(Bool), votes(Int), sortByVotes, none
14 | }
15 |
16 | class PHPostSorter {
17 |
18 | class func filter(_ store: Store, posts:[PHPost], by:[PHPostFilter]) -> [PHPost] {
19 | return by.reduce(posts, { (posts, filter) -> [PHPost] in
20 | switch(filter) {
21 |
22 | case .seen(let seen):
23 | let seenIds = store.state.seenPosts.postIds
24 | return posts.filter { (seen ? seenIds.contains($0.id) : !seenIds.contains($0.id) ) }
25 |
26 | case .votes(let votesCount):
27 | return posts.filter { $0.votesCount >= votesCount }
28 |
29 | case .sortByVotes:
30 | return posts.sorted(by: { $0.votesCount > $1.votesCount })
31 |
32 | case .none:
33 | return posts
34 | }
35 | })
36 | }
37 |
38 | class func sort(_ store: Store ,posts: [PHPost], votes: Int) -> [PHPost] {
39 | return filter(store, posts: posts, by: [.seen(false), .votes(votes), .sortByVotes]) + filter(store, posts: posts, by: [.seen(true), .sortByVotes])
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Source/PHSeenPostsModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSeenPostsModule.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/3/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | struct PHSeenPosts {
13 | var date: Date
14 | var postIds: Set
15 |
16 | func isSeen(_ post: PHPost) -> Bool {
17 | if PHDateFormatter.daysAgo(post.day) > 0 {
18 | return true
19 | }
20 |
21 | return postIds.contains(post.id)
22 | }
23 | }
24 |
25 | struct PHSeenPostsSetAction: Action {
26 | var seenPost: PHSeenPosts
27 | }
28 |
29 | struct PHMarkPostsAsSeenAction: Action {
30 | var posts: [PHPost]
31 | }
32 |
33 | func seenPostsReducer(_ action: Action, state: PHSeenPosts?) -> PHSeenPosts {
34 | let state = state ?? PHSeenPosts(date: Date(), postIds: Set())
35 |
36 | switch action {
37 | case let action as PHSeenPostsSetAction:
38 | return action.seenPost
39 |
40 | case let action as PHMarkPostsAsSeenAction:
41 | let date = (state.date as NSDate).isToday() ? state.date : Date()
42 | let postIds = (state.date as NSDate).isToday() ? state.postIds : Set()
43 |
44 | let ids = action.posts.map{ $0.id }
45 |
46 | return PHSeenPosts(date: date, postIds: postIds.union(ids))
47 |
48 | default:
49 | return state
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Source/PHPostsModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostsModule.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/28/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | struct PHPostsLoadAction: Action {
13 | var posts: [PHPost]
14 | }
15 |
16 | func postsReducer(_ action: Action, state: PHAppStatePosts?) -> PHAppStatePosts {
17 | let state = state ?? PHAppStatePosts(sections: [], lastUpdated: Date())
18 |
19 | switch action {
20 | case let action as PHPostsLoadAction:
21 | if action.posts.isEmpty {
22 | return state
23 | }
24 |
25 | let section = PHSection.section(action.posts)
26 |
27 | var newSections = state.sections
28 | var newLastUpdated = state.lastUpdated
29 |
30 | if PHDateFormatter.daysAgo(section.day) == 0 {
31 | if let firstSection = newSections.first, firstSection.day == section.day {
32 | newSections[0] = section
33 | } else {
34 | newSections.insert(section, at: 0)
35 | }
36 |
37 | newLastUpdated = Date()
38 | } else {
39 | newSections.append(PHSection.section(action.posts))
40 | }
41 |
42 | return PHAppStatePosts(sections: newSections, lastUpdated: newLastUpdated)
43 |
44 | default:
45 | return state
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Source/PHSettingsModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSettingsReducer.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/27/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import ReSwift
10 |
11 | struct PHSettings {
12 | var autologinEnabled: Bool
13 | var showsCount: Bool
14 | var filterCount: Int
15 | }
16 |
17 | struct PHSettingsSetAction: Action {
18 | var settings: PHSettings
19 | }
20 |
21 | struct PHSettingsActionAutoLogin: Action {
22 | var autologin: Bool
23 | }
24 |
25 | struct PHSettingsActionShowsCount: Action {
26 | var showsCount: Bool
27 | }
28 |
29 | struct PHSettngsActionFilterCount: Action {
30 | var filterCount: Int
31 | }
32 |
33 | func settingsReducer(_ action: Action, state: PHSettings?) -> PHSettings {
34 | var state = state ?? PHSettings(autologinEnabled: true, showsCount: true, filterCount: 10)
35 |
36 | switch action {
37 |
38 | case let action as PHSettingsSetAction:
39 | return action.settings
40 |
41 | case let action as PHSettingsActionAutoLogin:
42 | state.autologinEnabled = action.autologin
43 |
44 | return state
45 |
46 | case let action as PHSettingsActionShowsCount:
47 | state.showsCount = action.showsCount
48 |
49 | return state
50 |
51 | case let action as PHSettngsActionFilterCount:
52 | state.filterCount = action.filterCount
53 |
54 | return state
55 |
56 | default:
57 | return state
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Source/PHAPIOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/4/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | class PHAPIOperation {
13 |
14 | class func perform(_ store: Store, api: PHAPI, operation: @escaping PHAPIOperationClosure) {
15 | withToken(store, api: api) { (token, error) in
16 | operation(api, { (error) in
17 | if NSError.parseError(error) == NSError.unauthorizedError() {
18 | let token = PHToken(accessToken: "")
19 | store.dispatch( PHTokenGetAction(token: token) )
20 | }
21 |
22 | withToken(store, api: api, callback: { (token, error) in
23 | operation(api, { (error) in
24 | //TODO : Dispatch errror
25 | })
26 | })
27 | })
28 | }
29 | }
30 |
31 | fileprivate class func withToken(_ store: Store, api: PHAPI, callback: @escaping PHAPITokenCompletion) {
32 | if store.state.token.isValid {
33 | callback(store.state.token, nil)
34 | return
35 | }
36 |
37 | api.getToken { (token, error) in
38 | if let token = token {
39 | // TODO: Other mechanism to update token
40 |
41 | api.endpoint = PHAPIEndpoint(token: token)
42 | store.dispatch( PHTokenGetAction(token: token) )
43 | }
44 |
45 | callback(token, error)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Support/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon-16x16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon-16x16@2x.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "icon-32x32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "icon-32x32@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "icon-128x128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "icon-128x128@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "icon-256x256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "icon-256x256@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "icon-512x512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "icon-512x512@2x.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Source/PHLoadPostOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHLoadPostOperation.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/3/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | class PHLoadPostOperation {
13 |
14 | class func performNewer(_ store: Store? = store) {
15 | guard let store = store else {
16 | return
17 | }
18 |
19 | perform(store, api: PHAPI.sharedInstance, daysAgo: store.state.posts.today)
20 | }
21 |
22 | class func performOlder(_ store: Store? = store) {
23 | guard let store = store else {
24 | return
25 | }
26 |
27 | perform(store, api: PHAPI.sharedInstance, daysAgo: store.state.posts.nextDay)
28 | }
29 |
30 | class func perform(_ app: Store, api: PHAPI, daysAgo: Int) {
31 | if api.isThereOngoingRequest {
32 | return
33 | }
34 |
35 | let operation = createOperation(daysAgo) { (posts) in
36 | app.dispatch( PHPostsLoadAction(posts: posts) )
37 | }
38 |
39 | PHAPIOperation.perform(app, api: api, operation: operation)
40 | }
41 |
42 | class func createOperation(_ daysAgo: Int, complete: @escaping ([PHPost]) -> ()) -> PHAPIOperationClosure {
43 | return { (_ api: PHAPI,_ errorClosure: @escaping PHAPIErrorClosure) in
44 | api.getPosts(daysAgo, completion: { (posts, error) in
45 | if let error = error {
46 | errorClosure(error)
47 | return
48 | }
49 |
50 | complete(posts)
51 | })
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Source/Models/PHBundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHHardware.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/28/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHBundle {
12 |
13 | static var shortVersion: String? {
14 | return getValueFromInfoDictionary(for: "CFBundleShortVersionString", subjectType: String())
15 | }
16 |
17 | static var version: String? {
18 | return getValueFromInfoDictionary(for: "CFBundleVersion", subjectType: String())
19 | }
20 |
21 | // Referenced from https://developer.apple.com/library/mac/technotes/tn1103/_index.html
22 | class func systemUUID() -> String {
23 | let dev = IOServiceMatching("IOPlatformExpertDevice")
24 |
25 | let platformExpert: io_service_t = IOServiceGetMatchingService(kIOMasterPortDefault, dev)
26 |
27 | let serialNumberAsCFString = IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformUUIDKey as CFString!, kCFAllocatorDefault, 0)
28 |
29 | IOObjectRelease(platformExpert)
30 |
31 | let serialNumber: CFTypeRef = serialNumberAsCFString!.takeUnretainedValue()
32 |
33 | return serialNumber as? String ?? ""
34 | }
35 |
36 | class func ipAddress() -> String {
37 | let address = Host.current().addresses.filter{ $0.contains(".") && ($0 != "127.0.0.1") }
38 | return address.isEmpty ? "" : address.first!
39 | }
40 |
41 | fileprivate class func getValueFromInfoDictionary(for key: String, subjectType: T) -> T? {
42 | guard let infoDictionary = Bundle.main.infoDictionary else {
43 | return nil
44 | }
45 |
46 | return infoDictionary[key] as? T
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Source/Controllers/PHAdvancedSettingsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHUpdateSettingsViewController.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/18/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Sparkle
11 | import DateTools
12 |
13 | class PHAdvancedSettingsViewController: NSViewController, PHPreferencesWindowControllerProtocol {
14 |
15 | @IBOutlet weak var automaticallyCheckForUpdatesButton: NSButton!
16 | @IBOutlet weak var automaticallyDownloadUpdatesButton: NSButton!
17 |
18 | fileprivate let formatter = PHDateFormatter()
19 | fileprivate var updater: SUUpdater {
20 | return SUUpdater.shared()
21 | }
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | automaticallyCheckForUpdatesButton.setState(forBool: updater.automaticallyChecksForUpdates)
27 | automaticallyDownloadUpdatesButton.setState(forBool: updater.automaticallyDownloadsUpdates)
28 | }
29 |
30 | // MARK: Actions
31 |
32 | @IBAction func automaticallyCheckForUpdatesAction(_ sender: NSButton) {
33 | updater.automaticallyChecksForUpdates = sender.boolState
34 | }
35 |
36 | @IBAction func automaticallyDownloadUpdatesAction(_ sender: NSButton) {
37 | updater.automaticallyDownloadsUpdates = sender.boolState
38 | }
39 |
40 | // MARK: PHPreferencesWindowControllerProtocol
41 |
42 | func preferencesIdentifier() -> String {
43 | return "PHAdvancedSettingsViewController"
44 | }
45 |
46 | func preferencesTitle() -> String {
47 | return "Advanced"
48 | }
49 |
50 | func preferencesIcon() -> NSImage {
51 | return NSImage(named: NSImageNameAdvanced)!
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Support/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Product Hunt
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIconFile
12 |
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | 1.0.3
23 | CFBundleSignature
24 | ????
25 | CFBundleURLTypes
26 |
27 |
28 | CFBundleTypeRole
29 | Viewer
30 | CFBundleURLName
31 | com.producthunt.producthuntosx
32 | CFBundleURLSchemes
33 |
34 | producthunt
35 |
36 |
37 |
38 | CFBundleVersion
39 | 1.0.3
40 | LSApplicationCategoryType
41 | public.app-category.social-networking
42 | LSMinimumSystemVersion
43 | $(MACOSX_DEPLOYMENT_TARGET)
44 | LSUIElement
45 |
46 | NSHumanReadableCopyright
47 | Copyright © 2016 ProductHunt. All rights reserved.
48 | NSMainNibFile
49 | MainMenu
50 | NSPrincipalClass
51 | NSApplication
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Source/Models/Domain Objects/PHPost.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPost.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHPost {
12 |
13 | var id: Int
14 | var title: String
15 | var tagline: String
16 | var thumbnailUrl: URL
17 | var discussionUrl: URL
18 | var day: String
19 | var votesCount: Int
20 | var commentsCount: Int
21 | var createdAt: String
22 | var redirectUrl: URL
23 |
24 | init(id: Int, title: String, tagline: String, thumbnailUrl: URL, discussionUrl: URL, day: String, votesCount: Int, commentsCount: Int, createdAt: String, redirectUrl: URL) {
25 | self.id = id
26 | self.title = title
27 | self.tagline = tagline
28 | self.thumbnailUrl = thumbnailUrl
29 | self.discussionUrl = discussionUrl
30 | self.day = day
31 | self.votesCount = votesCount
32 | self.commentsCount = commentsCount
33 | self.createdAt = createdAt
34 | self.redirectUrl = redirectUrl
35 | }
36 |
37 | func description() -> [String: Any] {
38 | return [
39 | "id" : id,
40 | "name" : title,
41 | "tagline" : tagline,
42 | "day" : day,
43 | "discussion_url" : discussionUrl.absoluteString,
44 | "thumbnail" : ["image_url" : thumbnailUrl.absoluteString],
45 | "votes_count" : votesCount,
46 | "comments_count" : commentsCount,
47 | "created_at" : createdAt,
48 | "redirect_url" : redirectUrl.absoluteString
49 | ]
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/Source/Models/Domain Objects/PHPostFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostFactory.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension PHPost {
12 |
13 | class func post(fromDictionary dictionary: [String: Any]) -> PHPost? {
14 | guard
15 | let id = dictionary["id"] as? Int,
16 | let title = dictionary["name"] as? String,
17 | let tagline = dictionary["tagline"] as? String,
18 | let day = dictionary["day"] as? String,
19 | let discussionPath = dictionary["discussion_url"] as? String,
20 | let thumbnail = dictionary["thumbnail"] as? [String: AnyObject],
21 | let thumbnailPath = thumbnail["image_url"] as? String,
22 | let votesCount = dictionary["votes_count"] as? Int,
23 | let commentsCount = dictionary["comments_count"] as? Int,
24 | let createdAt = dictionary["created_at"] as? String,
25 | let redirectUrl = dictionary["redirect_url"] as? String
26 | else {
27 | return nil
28 | }
29 |
30 | let thumbnailURL = URL(string: thumbnailPath)!
31 | let discussionURL = URL(string: discussionPath)!
32 | let redirectURL = URL(string: redirectUrl)!
33 |
34 | return PHPost(id: id,title: title, tagline: tagline, thumbnailUrl: thumbnailURL, discussionUrl: discussionURL, day: day, votesCount: votesCount, commentsCount: commentsCount, createdAt: createdAt, redirectUrl: redirectURL)
35 | }
36 |
37 | class func posts(fromArray array: [[String: Any]]) -> [PHPost] {
38 | return array.flatMap{ PHPost.post(fromDictionary: $0)! }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/Support/PHFakeFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHFakeFactory.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/31/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHFakeFactory {
12 |
13 | static let sharedInstance = PHFakeFactory()
14 |
15 | fileprivate var fakePostId = 0
16 |
17 | func token() -> PHToken {
18 | return PHToken(accessToken: "FakeToken")
19 | }
20 |
21 | func post(_ daysAgo: TimeInterval = 0.days, votes: Int = 20, commentsCount: Int = 0) -> PHPost {
22 | let id = fakePostId
23 | let title = "Title \(fakePostId)"
24 | let tagline = "Tagline \(fakePostId)"
25 | let url = URL(string: "https//example.com/\(fakePostId)")!
26 | let day = daysAgoAsString(daysAgo)
27 | let redirectUrl = URL(string: "https//example.com/\(fakePostId)")!
28 |
29 | defer {
30 | fakePostId += 1
31 | }
32 |
33 | return PHPost(id: id,title: title, tagline: tagline, thumbnailUrl: url, discussionUrl: url, day: day, votesCount: votes, commentsCount: commentsCount, createdAt: createdAt(), redirectUrl: redirectUrl)
34 | }
35 |
36 | fileprivate func daysAgoAsString(_ days: TimeInterval) -> String {
37 | let date = Date(timeIntervalSinceNow: -days)
38 |
39 | let dateformatter = DateFormatter()
40 | dateformatter.dateFormat = "yyyy-MM-dd"
41 |
42 | return dateformatter.string(from: date)
43 | }
44 |
45 | fileprivate func createdAt() -> String {
46 | let formatter = DateFormatter()
47 |
48 | formatter.timeZone = TimeZone(abbreviation: "PSD")
49 |
50 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
51 |
52 | return formatter.string(from: Date())
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Product Hunt for Mac
2 |
3 | > This is the official [Product Hunt](http://www.producthunt.com) for Mac.
4 |
5 | 
6 |
7 | Product Hunt is the place to discover your next favorite thing, surfacing the latest in tech, books, games, and podcasts.
8 |
9 | Also check out our iOS app and Chrome extension at [http://producthunt.com/apps](http://producthunt.com/apps).
10 |
11 | ## Download
12 |
13 | You can also download Product Hunt for Mac [here](https://s3.amazonaws.com/producthunt/mac/ProductHunt.dmg)
14 |
15 | ## Development
16 |
17 | * Clone the repository:
18 |
19 | ```
20 | $ git clone git@github.com:producthunt/producthunt-osx.git
21 | $ cd producthunt-osx
22 | ```
23 |
24 | * Copy configuration templates:
25 |
26 | ```
27 | cp Source/Config/Keys-Example.xcconfig Source/Config/Keys.xcconfig
28 | ```
29 |
30 | API Keys generated from https://www.producthunt.com/v1/oauth/applications
31 |
32 | * Install CocoaPods Dependencies
33 |
34 | ```
35 | $ pod install
36 | ```
37 |
38 | * Open `Product\ Hunt.xcworkspace`
39 |
40 | ## Contributing
41 |
42 | 1. Fork it
43 | 2. Create your feature branch (`git checkout -b my-new-feature`)
44 | 3. Commit your changes (`git commit -am 'Add some feature'`)
45 | 4. Push to the branch (`git push origin my-new-feature`)
46 | 5. Run the tests
47 | 6. Create new Pull Request
48 |
49 | ## License
50 |
51 | [](https://www.producthunt.com)
52 |
53 | ```
54 | _________________
55 | < The MIT License >
56 | -----------------
57 | \ ^__^
58 | \ (oo)\_______
59 | (__)\ )\/\
60 | ||----w |
61 | || ||
62 | ```
63 |
64 | **[MIT License](https://github.com/producthunt/PHImageKit/blob/master/LICENSE)**
65 |
--------------------------------------------------------------------------------
/Source/Models/PHAnalitycsAPIEndpoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAnalitycsAPIEndpoint.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AFNetworking
11 |
12 | class PHAnalitycsAPIEndpoint {
13 |
14 | static let kPHAnalitycsEndpointHost = "https://api.segment.io"
15 |
16 | fileprivate let manager = AFHTTPSessionManager(baseURL: URL(string: "\(kPHAnalitycsEndpointHost)/v1"))
17 |
18 | init(key: String) {
19 | manager.responseSerializer = AFJSONResponseSerializer()
20 | manager.requestSerializer = AFJSONRequestSerializer()
21 |
22 | manager.requestSerializer.setValue("application/json", forHTTPHeaderField: "Accept")
23 | manager.requestSerializer.setValue("application/json", forHTTPHeaderField: "Content-Type")
24 | manager.requestSerializer.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
25 | manager.requestSerializer.setValue(Credentials.basic(key, password: ""), forHTTPHeaderField: "Authorization")
26 | }
27 |
28 | func get(_ url: String, parameters: [String: Any]?, completion: PHAPIEndpointCompletion? = nil) {
29 | manager.get(url, parameters: parameters, progress: nil, success: { (task, response) in
30 | completion?((response as? [String: AnyObject]), nil)
31 | }) { (task, error) in
32 | print(error)
33 | completion?(nil, error as NSError?)
34 | }
35 | }
36 |
37 | func post(_ url: String, parameters: [String: Any]?, completion: PHAPIEndpointCompletion? = nil) {
38 | manager.post(url, parameters: parameters, progress: nil, success: { (task, response) in
39 | completion?((response as? [String: AnyObject]), nil)
40 | }) { (task, error) in
41 | print(error)
42 | completion?(nil, error as NSError?)
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Source/Models/API/PHAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAPI.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PHAPI {
12 |
13 | static let sharedInstance = PHAPI()
14 |
15 | var endpoint = PHAPIEndpoint(token: store.state.token)
16 |
17 | fileprivate(set) var isThereOngoingRequest = false
18 |
19 | func getToken(_ completion: @escaping PHAPITokenCompletion) {
20 | let params = ["client_id" : kPHAppID, "client_secret": kPHAppSecret, "grant_type": "client_credentials"]
21 |
22 | endpoint.post("oauth/token", parameters: params) { (response, error) -> Void in
23 | completion(PHToken.token(fromDictionary: response), error)
24 | }
25 | }
26 |
27 | func getPosts(_ daysAgo: Int, completion: @escaping PHAPIPostCompletion) {
28 | getPostsPosted(daysAgo, retries: 20, completion: completion)
29 | }
30 |
31 | fileprivate func getPostsPosted(_ daysAgo: Int, retries: Int, completion: @escaping PHAPIPostCompletion) {
32 | if retries == 0 {
33 | isThereOngoingRequest = false
34 | completion([], NSError.unauthorizedError())
35 | return
36 | }
37 |
38 | isThereOngoingRequest = true
39 |
40 | self.endpoint.get("posts", parameters: ["days_ago": daysAgo, "search[category]": "all"]) { (response, error) -> Void in
41 | self.isThereOngoingRequest = false
42 |
43 | if let response = response, let rawPosts = response["posts"] as? [[String : AnyObject]] {
44 | completion(PHPost.posts(fromArray: rawPosts), nil)
45 | } else if let error = error {
46 | completion([PHPost](), error)
47 | } else {
48 | self.getPostsPosted(daysAgo + 1, retries: retries - 1, completion: completion)
49 | }
50 | }
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Source/Models/PHPostsDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostsDataSource.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ReSwift
11 |
12 | protocol PHDataSourceDelegate {
13 | func contentChanged()
14 | }
15 |
16 | class PHPostsDataSource: StoreSubscriber {
17 |
18 | var delegate: PHDataSourceDelegate?
19 |
20 | fileprivate var store: Store
21 | fileprivate var content = [AnyObject]()
22 |
23 | init(store: Store) {
24 | self.store = store
25 |
26 | store.subscribe(self)
27 | }
28 |
29 | deinit {
30 | store.unsubscribe(self)
31 | }
32 |
33 | func newState(state: PHAppState) {
34 | content = flatContent(from: state.posts.sections)
35 | delegate?.contentChanged()
36 | }
37 |
38 | func numberOfRows() -> Int {
39 | return content.count
40 | }
41 |
42 | func data(atIndex index: Int) -> AnyObject? {
43 | return content[index]
44 | }
45 |
46 | func isGroup(atIndex index: Int) -> Bool {
47 | return (content[index] as? String) != nil
48 | }
49 |
50 | func loadNewer() {
51 | PHLoadPostOperation.performNewer(store)
52 | }
53 |
54 | func loadOlder() {
55 | PHLoadPostOperation.performOlder(store)
56 | }
57 |
58 | fileprivate func flatContent(from content: [PHSection]) -> [AnyObject] {
59 | let formatter = PHDateFormatter()
60 | var output = [AnyObject]()
61 |
62 | content.forEach { (section) in
63 | output.append(formatter.format(withDateString: section.day) as AnyObject? ?? "" as AnyObject)
64 |
65 | PHPostSorter.sort(store, posts: section.posts, votes: store.state.settings.filterCount).forEach({ (post) in
66 | output.append(post)
67 | })
68 | }
69 |
70 | return output
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Source/Models/API/PHAPIEndpoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAPIEndpoint.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import AFNetworking
11 |
12 | typealias PHAPIEndpointCompletion = (_ response: [String: Any]?, _ error: NSError?) -> Void
13 |
14 | class PHAPIEndpoint {
15 |
16 | static let kPHEndpointHost = "https://api.producthunt.com"
17 |
18 | fileprivate let manager = AFHTTPSessionManager(baseURL: URL(string: "\(kPHEndpointHost)/v1"))
19 |
20 | init(token: PHToken?) {
21 | manager.responseSerializer = AFJSONResponseSerializer()
22 | manager.requestSerializer = AFJSONRequestSerializer()
23 |
24 | manager.requestSerializer.setValue("application/json", forHTTPHeaderField: "Accept")
25 | manager.requestSerializer.setValue("application/json", forHTTPHeaderField: "Content-Type")
26 |
27 | if let token = token {
28 | manager.requestSerializer.setValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization")
29 | }
30 | }
31 |
32 | func get(_ url: String, parameters: [String: Any]?, completion: @escaping PHAPIEndpointCompletion) {
33 | manager.get(url, parameters: parameters, progress: nil, success: { (task, response) in
34 | completion((response as? [String: AnyObject]), nil)
35 | }) { (task, error) in
36 | print(error)
37 | completion(nil, error as NSError?)
38 | }
39 | }
40 |
41 | func post(_ url: String, parameters: [String: Any]?, completion: @escaping PHAPIEndpointCompletion) {
42 | manager.post(url, parameters: parameters, progress: nil, success: { (task, response) in
43 | completion((response as? [String: AnyObject]), nil)
44 | }) { (task, error) in
45 | print(error)
46 | completion(nil, error as NSError?)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Source/Views/PHLoadingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHLoadingView.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/17/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | enum LoadingState {
12 | case loading, error, empty, idle
13 | }
14 |
15 | class PHLoadingView: NSView {
16 |
17 | @IBOutlet weak var loadingIndicator: NSProgressIndicator!
18 | @IBOutlet weak var loadingLabel: NSTextField!
19 | @IBOutlet weak var reload: PHButton!
20 |
21 | var state: LoadingState = .idle {
22 | didSet {
23 | switch state {
24 | case .loading:
25 | isHidden = false
26 | reload.isHidden = true
27 | loadingIndicator.isHidden = false
28 | loadingIndicator.startAnimation(nil)
29 | loadingLabel.stringValue = "Hunting down new posts..."
30 |
31 | case .error:
32 | isHidden = false
33 | reload.isHidden = false
34 | loadingIndicator.isHidden = true
35 | loadingIndicator.stopAnimation(nil)
36 | loadingLabel.stringValue = "Something went wrong 😿"
37 |
38 | case .empty:
39 | isHidden = false
40 | reload.isHidden = false
41 | loadingIndicator.isHidden = true
42 | loadingIndicator.stopAnimation(nil)
43 | loadingLabel.stringValue = "Nothing to show 😿"
44 |
45 | case .idle:
46 | isHidden = true
47 | loadingIndicator.stopAnimation(nil)
48 | }
49 | }
50 | }
51 |
52 | override func awakeFromNib() {
53 | super.awakeFromNib()
54 |
55 | wantsLayer = true
56 | layer?.backgroundColor = NSColor.white.cgColor
57 | }
58 |
59 | @IBAction func toggleReloadButton(_ sender: NSView) {
60 | state = .loading
61 | PHLoadPostOperation.performNewer()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Source/Actions/PHShareAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHShareAction.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 4/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Kingfisher
11 | import ReSwift
12 |
13 | class PHShareAction: NSObject, NSSharingServiceDelegate {
14 |
15 | static let sharedInstance = PHShareAction()
16 |
17 | func performTwitter(_ post: PHPost?) {
18 | guard let post = post else {
19 | return
20 | }
21 |
22 | store.dispatch( PHTrackPostShare(post: post, medium: "twitter") )
23 |
24 | KingfisherManager.shared.retrieveImage(with: post.thumbnailUrl, options: nil, progressBlock: nil, completionHandler: { (image, error, cacheType, imageURL) in
25 | self.perform(NSSharingServiceNamePostOnTwitter, title: PHShareMessage.message(fromPost: post), url: post.discussionUrl, image: image)
26 | })
27 | }
28 |
29 | func performFacebook(_ post: PHPost?) {
30 | guard let post = post else {
31 | return
32 | }
33 |
34 | store.dispatch( PHTrackPostShare(post: post, medium: "facebook") )
35 |
36 | KingfisherManager.shared.retrieveImage(with: post.thumbnailUrl, options: nil, progressBlock: nil, completionHandler: { (image, error, cacheType, imageURL) in
37 | self.perform(NSSharingServiceNamePostOnFacebook, title: PHShareMessage.message(fromPost: post), url: post.discussionUrl, image: image)
38 | })
39 | }
40 |
41 | fileprivate func perform(_ service: String, title: String, url: URL, image: NSImage?) {
42 | if let service = NSSharingService(named: service) {
43 | service.delegate = self
44 |
45 | var items = [AnyObject]()
46 |
47 | items.append(title as AnyObject)
48 |
49 | if let image = image {
50 | items.append(image)
51 | }
52 |
53 | service.perform(withItems: items)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHSeenPostsModuleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHSeenPostsModuleTests.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/4/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import ReSwift
11 |
12 | class PHSeenPostsModuleTests: PHTestCase {
13 |
14 | struct BootStrapAction: Action {}
15 |
16 | func testThatReturnsState() {
17 | let reducer = seenPostsReducer(BootStrapAction(), state: nil)
18 |
19 | XCTAssertNotNil(reducer)
20 | }
21 |
22 | func testThatmarksPostAsSeen() {
23 | let post = fake.post()
24 |
25 | let seenPosts = PHSeenPosts(date: Date(timeIntervalSinceNow: 0), postIds: [])
26 |
27 | let action = PHMarkPostsAsSeenAction(posts: [post])
28 |
29 | let reducer = seenPostsReducer(action, state: seenPosts)
30 |
31 | XCTAssertTrue( reducer.postIds.contains(post.id) )
32 | }
33 |
34 | func testThatItResetsSeenPostsState() {
35 | let daysAgo = 1.days
36 |
37 | let yesterdayPost = fake.post(daysAgo, votes: 0, commentsCount: 0)
38 |
39 | let post = fake.post()
40 |
41 | let seenPosts = PHSeenPosts(date: Date(timeIntervalSinceNow: daysAgo), postIds: Set([yesterdayPost.id]))
42 |
43 | let action = PHMarkPostsAsSeenAction(posts: [post])
44 |
45 | let reducer = seenPostsReducer(action, state: seenPosts)
46 |
47 | XCTAssertTrue(reducer.date.isToday())
48 | XCTAssertEqual(reducer.postIds.count, 1)
49 | XCTAssertTrue(reducer.isSeen(yesterdayPost))
50 | XCTAssertTrue(reducer.isSeen(post))
51 | }
52 |
53 | func testThatAddsOnlyUnqueIds() {
54 | let post = fake.post()
55 |
56 | let seenPosts = PHSeenPosts(date: Date(), postIds: Set([post.id]))
57 |
58 | let action = PHMarkPostsAsSeenAction(posts: [post])
59 |
60 | let reducer = seenPostsReducer(action, state: seenPosts)
61 |
62 | XCTAssertTrue(reducer.isSeen(post))
63 | XCTAssertEqual(reducer.postIds.count, 1)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHDateFormatterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHDateFormatter.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class PHDateFormatterTests: PHTestCase {
12 |
13 | let formatter = PHDateFormatter()
14 |
15 | func testThatItFormatsToday() {
16 | let dateformatter = DateFormatter()
17 | dateformatter.dateFormat = "yyyy-MM-dd"
18 |
19 | let dateAsString = dateformatter.string(from: Date())
20 |
21 | let formattedString = formatter.format(withDateString: dateAsString)!
22 |
23 | XCTAssertTrue(formattedString.range(of: "Today") != nil)
24 | }
25 |
26 | func testThatItFormatsYesterday() {
27 | let date = Date(timeIntervalSinceNow: -(60*60*24 + 1))
28 |
29 | let dateformatter = DateFormatter()
30 | dateformatter.dateFormat = "yyyy-MM-dd"
31 |
32 | let dateAsString = dateformatter.string(from: date)
33 |
34 | let formattedString = formatter.format(withDateString: dateAsString)!
35 |
36 | XCTAssertTrue(formattedString.range(of: "Yesterday") != nil)
37 | }
38 |
39 | func testThatAddsStSuffux() {
40 | let dateString = "2016-01-31"
41 |
42 | let formattedString = formatter.format(withDateString: dateString)!
43 |
44 | XCTAssertTrue(formattedString.range(of: "31st") != nil)
45 | }
46 |
47 | func testThatAddsRdSuffix() {
48 | let dateString = "2016-01-03"
49 |
50 | let formattedString = formatter.format(withDateString: dateString)!
51 |
52 | XCTAssertTrue(formattedString.range(of: "3rd") != nil)
53 | }
54 |
55 | func testThatHandlesInvalidDate() {
56 | let date = "wrong date"
57 |
58 | XCTAssertNil(formatter.format(withDateString: date))
59 | }
60 |
61 | func testThatItReturnsDaysAgo() {
62 | let date = Date(timeIntervalSinceNow: -1.day)
63 |
64 | let dateformatter = DateFormatter()
65 | dateformatter.dateFormat = "yyyy-MM-dd"
66 |
67 | let dateAsString = dateformatter.string(from: date)
68 |
69 | XCTAssertEqual(formatter.daysAgo(fromDateAsString: dateAsString), 1)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/Support/PHFakeEndpoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHFakeEndpoint.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/30/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct PHTestFakeAPIEndpointStub {
12 |
13 | var method: String
14 | var url: String
15 | var parameters: [String : AnyObject]
16 | var response: [String : AnyObject]?
17 | var error: NSError?
18 |
19 | func match(_ method: String, url: String, parameters: [String : AnyObject]) -> Bool {
20 | return self.method == method && self.url == url && self.parameters.description == parameters.description
21 | }
22 | }
23 |
24 | class PHAPIFakeEndpoint: PHAPIEndpoint {
25 |
26 | fileprivate var fakes = [PHTestFakeAPIEndpointStub]()
27 |
28 | override init(token: PHToken?) {
29 | super.init(token: token)
30 | }
31 |
32 | func addFake(_ method: String, url: String, parameters: [String: AnyObject], response: [String: AnyObject]?, error: NSError?) {
33 | let stub = PHTestFakeAPIEndpointStub(method: method, url: url, parameters: parameters, response: response, error: error)
34 | fakes.append(stub)
35 | }
36 |
37 | override func get(_ url: String, parameters: [String: AnyObject]?, completion: PHAPIEndpointCompletion) {
38 | handleFakeMethod("GET", url: url, parameters: parameters ?? [String: AnyObject](), completion: completion)
39 | }
40 |
41 | override func post(_ url: String, parameters: [String: AnyObject]?, completion: PHAPIEndpointCompletion) {
42 | handleFakeMethod("POST", url: url, parameters: parameters ?? [String: AnyObject](), completion: completion)
43 | }
44 |
45 | fileprivate func handleFakeMethod(_ method: String, url: String, parameters: [String : AnyObject], completion: PHAPIEndpointCompletion) {
46 | guard let stub = fakes.filter({ $0.match(method, url: url, parameters: parameters) }).first else {
47 | NSException(name: NSExceptionName(rawValue: "PHTestFakeAPIEndpoingFailure"), reason: "No response found for \(method) \(url) (\(parameters.description))", userInfo: nil).raise()
48 | return
49 | }
50 |
51 | completion(stub.response, stub.error)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/LaunchAtLogin/LLManager.m:
--------------------------------------------------------------------------------
1 | //
2 | // LLManager.m
3 | // LaunchAtLogin
4 | //
5 | // Created by David Keegan on 4/20/12.
6 | // Copyright (c) 2012 David Keegan.
7 | // Copyright (c) 2014 Jan Weiß.
8 | // Some rights reserved:
9 | //
10 |
11 | #import "LLManager.h"
12 | #import
13 |
14 | NSString * const LLManagerSetLaunchAtLoginFailedNotification = @"LLManagerSetLaunchAtLoginFailedNotification";
15 | NSString * const LLHelperBundleIdentifier = @"com.producthunt.producthuntosx.LaunchAtLoginHelper";
16 |
17 | @implementation LLManager
18 |
19 | + (BOOL)launchAtLogin{
20 | BOOL launch = false;
21 |
22 | CFArrayRef cfJobs = SMCopyAllJobDictionaries(kSMDomainUserLaunchd);
23 |
24 | if(cfJobs == NULL) {
25 | return false;
26 | }
27 |
28 | NSArray *jobs = CFBridgingRelease(cfJobs);
29 |
30 | if([jobs count]){
31 | for(NSDictionary *job in jobs) {
32 | if([job[@"Label"] isEqualToString:LLHelperBundleIdentifier]){
33 | launch = [job[@"OnDemand"] boolValue];
34 | break;
35 | }
36 | }
37 | }
38 |
39 | return launch;
40 | }
41 |
42 | + (void)setLaunchAtLogin:(BOOL)value {
43 | [self setLaunchAtLogin:value notifyOnFailure:false];
44 | }
45 |
46 | + (void)setLaunchAtLogin:(BOOL)value notifyOnFailure:(BOOL)wantFailureNotification {
47 | CFStringRef LLHelperBundleIdentifierCF = (__bridge CFStringRef)LLHelperBundleIdentifier;
48 |
49 | if(!SMLoginItemSetEnabled(LLHelperBundleIdentifierCF, value)){
50 | if(wantFailureNotification){
51 | [[NSNotificationCenter defaultCenter] postNotificationName:LLManagerSetLaunchAtLoginFailedNotification object:self];
52 | } else {
53 | NSLog(@"SMLoginItemSetEnabled failed!");
54 | }
55 | }
56 | }
57 |
58 | #pragma mark - Bindings support
59 |
60 | - (BOOL)launchAtLogin {
61 | return [[self class] launchAtLogin];
62 | }
63 |
64 | - (void)setLaunchAtLogin:(BOOL)launchAtLogin {
65 | [self willChangeValueForKey:@"launchAtLogin"];
66 | [[self class] setLaunchAtLogin:launchAtLogin notifyOnFailure:self.notifyIfSetLaunchAtLoginFailed];
67 | [self didChangeValueForKey:@"launchAtLogin"];
68 | }
69 |
70 | @end
71 |
--------------------------------------------------------------------------------
/Source/Views/PHButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHButton.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/24/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class PHButton: NSButton {
12 |
13 | fileprivate let cursor = NSCursor.pointingHand()
14 | fileprivate var normalStateImage: NSImage?
15 | fileprivate var highlightedStateImage: NSImage?
16 | fileprivate var trackingArea: NSTrackingArea?
17 |
18 | override func resetCursorRects() {
19 | addCursorRect(bounds, cursor: cursor)
20 | cursor.set()
21 | }
22 |
23 | override init(frame frameRect: NSRect) {
24 | super.init(frame: frameRect)
25 | commonInit()
26 | }
27 |
28 | required init?(coder: NSCoder) {
29 | super.init(coder: coder)
30 | commonInit()
31 | }
32 |
33 | override func awakeFromNib() {
34 | super.awakeFromNib()
35 | commonInit()
36 | }
37 |
38 | func commonInit() {
39 | }
40 |
41 | func setImages(_ normalImage: String, highlitedImage: String) {
42 | self.setButtonType(.momentaryChange)
43 |
44 | normalStateImage = NSImage(named: normalImage)
45 | highlightedStateImage = NSImage(named: highlitedImage)
46 | }
47 |
48 | func resetTrackingArea() {
49 | trackingArea = nil
50 |
51 | if let normalStateImage = normalStateImage {
52 | image = normalStateImage
53 | }
54 | }
55 |
56 | fileprivate func createTrackingAreaIfNeeded() {
57 | if trackingArea == nil {
58 | trackingArea = NSTrackingArea(rect: CGRect.zero, options: [.inVisibleRect, .mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil)
59 | }
60 | }
61 |
62 | override func updateTrackingAreas() {
63 | super.updateTrackingAreas()
64 |
65 | createTrackingAreaIfNeeded()
66 |
67 | if !trackingAreas.contains(trackingArea!) {
68 | addTrackingArea(trackingArea!)
69 | }
70 | }
71 |
72 | override func mouseEntered(with theEvent: NSEvent) {
73 | if let highlightedImage = highlightedStateImage {
74 | image = highlightedImage
75 | }
76 | }
77 |
78 | override func mouseExited(with theEvent: NSEvent) {
79 | if let normalStateImage = normalStateImage {
80 | image = normalStateImage
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHPostsModuleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostsModuleTests.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/4/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import ReSwift
11 |
12 | class PHPostsModuleTests: PHTestCase {
13 |
14 | struct BootStrapAction: Action {}
15 |
16 | func testThatReturnsState() {
17 | let posts = postsReducer(BootStrapAction(), state: nil)
18 |
19 | XCTAssertNotNil(posts)
20 | }
21 |
22 | func testThatAcceptsEmptyArray() {
23 | let state = PHAppStatePosts(sections: [], lastUpdated: Date())
24 |
25 | let reducer = postsReducer(PHPostsLoadAction(posts: []) , state: state)
26 |
27 | XCTAssertNotNil(reducer)
28 | }
29 |
30 | func testThatOldPostsAppendsSectionAtTheEnd() {
31 | let section = PHSection.section( [fake.post(-1.day, votes: 10, commentsCount: 10)] )
32 |
33 | let state = PHAppStatePosts(sections: [section], lastUpdated: Date())
34 |
35 | let post = fake.post(-2.day, votes: 10, commentsCount: 10)
36 |
37 | let action = PHPostsLoadAction(posts: [post])
38 |
39 | let reducer = postsReducer(action, state: state)
40 |
41 | XCTAssertEqual(reducer.sections.count, 2)
42 | XCTAssertEqual(reducer.sections.last!.posts.first!.id, post.id)
43 | }
44 |
45 | func testThatNewPostsAppendsSectionAtFront() {
46 | let section = PHSection.section( [fake.post(-1.day, votes: 10, commentsCount: 10)] )
47 |
48 | let state = PHAppStatePosts(sections: [section], lastUpdated: Date())
49 |
50 | let post = fake.post()
51 |
52 | let action = PHPostsLoadAction(posts: [post])
53 |
54 | let reducer = postsReducer(action, state: state)
55 |
56 | XCTAssertEqual(reducer.sections.count, 2)
57 | XCTAssertEqual(reducer.sections.first!.posts.first!.id, post.id)
58 | }
59 |
60 | func testThatReplacesTodaySection() {
61 | let section = PHSection.section( [fake.post()] )
62 |
63 | let state = PHAppStatePosts(sections: [section], lastUpdated: Date())
64 |
65 | let post = fake.post()
66 |
67 | let action = PHPostsLoadAction(posts: [post])
68 |
69 | let reducer = postsReducer(action, state: state)
70 |
71 | XCTAssertEqual(reducer.sections.count, 1)
72 | XCTAssertEqual(reducer.sections.first!.posts.first!.id, post.id)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Source/Controllers/PHGeneralSettingsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHGeneralSettingsViewController.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/22/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import ServiceManagement
11 | import ReSwift
12 |
13 | class PHGeneralSettingsViewController: NSViewController, PHPreferencesWindowControllerProtocol, StoreSubscriber {
14 |
15 | @IBOutlet weak var startAtLoginButton: NSButton!
16 | @IBOutlet weak var showsCountButton: NSButton!
17 | @IBOutlet weak var versionLabel: NSTextField!
18 | @IBOutlet weak var filterVotesLabel: NSTextField!
19 | @IBOutlet weak var filterVotesSlider: NSSlider!
20 |
21 | var currenSliderValue: Int {
22 | return Int(5 * Int(round(filterVotesSlider.floatValue / 5.0)))
23 | }
24 |
25 | override func viewWillAppear() {
26 | super.viewWillAppear()
27 |
28 | filterVotesSlider.minValue = 0
29 | filterVotesSlider.maxValue = 100
30 |
31 | if let version = PHBundle.version {
32 | versionLabel.stringValue = "Product Hunt for OSX \(version)"
33 | }
34 |
35 | store.subscribe(self)
36 |
37 | newState(state: store.state)
38 | }
39 |
40 | override func viewWillDisappear() {
41 | super.viewWillDisappear()
42 | store.unsubscribe(self)
43 | }
44 |
45 | @IBAction func startAtLoginAction(_ sender: NSButton) {
46 | store.dispatch( PHSettingsActionAutoLogin(autologin: sender.boolState) )
47 | }
48 |
49 | @IBAction func showPostsCountAction(_ sender: NSButton) {
50 | store.dispatch( PHSettingsActionShowsCount(showsCount: sender.boolState) )
51 | }
52 |
53 | @IBAction func filterSliderValueChanged(_ sender: NSSlider) {
54 | store.dispatch( PHSettngsActionFilterCount(filterCount: currenSliderValue) )
55 | }
56 |
57 | func newState(state: PHAppState) {
58 | startAtLoginButton.setState(forBool: state.settings.autologinEnabled)
59 | showsCountButton.setState(forBool: state.settings.showsCount)
60 |
61 | filterVotesSlider.integerValue = state.settings.filterCount
62 |
63 | filterVotesLabel.stringValue = currenSliderValue == 0 ? "Don't filter out posts by votes" : "Filter out posts with less than \(currenSliderValue) votes"
64 | }
65 |
66 | // MARK: PHPreferencesWindowControllerProtocol
67 |
68 | func preferencesIdentifier() -> String {
69 | return "PHGeneralSettingsViewControllerIdentifier"
70 | }
71 |
72 | func preferencesTitle() -> String {
73 | return "General"
74 | }
75 |
76 | func preferencesIcon() -> NSImage {
77 | return NSImage(named: NSImageNamePreferencesGeneral)!
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Source/Models/PHAnalitycs.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHAnalitycs.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 5/5/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias PHAnalitycsProperties = [String: Any]
12 |
13 | class PHAnalitycs {
14 |
15 | static let sharedInstance = PHAnalitycs()
16 |
17 | fileprivate let kTrackEventVisit = "visit"
18 | fileprivate let kTrackEventClick = "click"
19 | fileprivate let kTrackEventShare = "share"
20 | fileprivate let kTrackPlatform = "osx"
21 |
22 | fileprivate var api = PHAnalitycsAPI()
23 |
24 | fileprivate var context: PHAnalitycsProperties {
25 | return [ "uuid" : PHBundle.systemUUID() as AnyObject ]
26 | }
27 |
28 | fileprivate var timestamp: String {
29 | return (Date() as NSDate).iso8601String() ?? ""
30 | }
31 |
32 | func trackClickPost(_ id: Int) {
33 | trackClick("post", subjectId: id)
34 | }
35 |
36 | func trackClick(_ subjectType: String, subjectId: Int) {
37 | let properties: PHAnalitycsProperties = [
38 | "event" : kTrackEventClick,
39 | "properties": [
40 | "subject_type" : subjectType,
41 | "subject_id" : subjectId,
42 | "platform" : kTrackPlatform,
43 | "page" : "home",
44 | "deeplink_uri_pattern" : "/home",
45 | "deeplink_uri_full" : "/home",
46 | ],
47 | "context" : context,
48 | "timestamp" : timestamp
49 | ]
50 |
51 | api.track(properties)
52 | }
53 |
54 | func trackShare(_ subjectType: String, subjectId: Int, medium: String) {
55 | let properties: PHAnalitycsProperties = [
56 | "event" : kTrackEventShare,
57 | "properties": [
58 | "subject_type" : subjectType,
59 | "subject_id" : subjectId,
60 | "platform" : kTrackPlatform,
61 | "medium" : medium,
62 | "page" : "home",
63 | "deeplink_uri_pattern" : "/home",
64 | "deeplink_uri_full" : "/home",
65 | ],
66 | "context" : context,
67 | "timestamp" : timestamp
68 | ]
69 |
70 | api.track(properties)
71 | }
72 |
73 | func trackVisit(_ page: String) {
74 | let properties: PHAnalitycsProperties = [
75 | "event" : kTrackEventVisit as AnyObject,
76 | "page" : page as AnyObject,
77 | "timestamp" : timestamp as AnyObject
78 | ]
79 |
80 | api.visit(properties)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Source/Models/PHDateFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHDateFormatter.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import DateTools
11 | import ISO8601
12 |
13 | class PHDateFormatter {
14 |
15 | class func daysAgo(_ dateString: String) -> Int {
16 | return PHDateFormatter().daysAgo(fromDateAsString: dateString)
17 | }
18 |
19 | fileprivate let formatter = DateFormatter()
20 | fileprivate var units: [Int : String] {
21 | return [0: "th", 1: "st", 21: "st", 31: "st", 2: "nd", 22: "nd", 3: "rd", 23: "rd"]
22 | }
23 |
24 | func format(withDateString dateString: String) -> String? {
25 | guard let date = parseDate(dateString) else {
26 | return nil
27 | }
28 |
29 | return format(date)
30 | }
31 |
32 | func format(_ date: Date) -> String {
33 | let day = getDayName(date)
34 | let month = getMonthName(date)
35 | let suffix = getDaySuffix(date)
36 |
37 | return "\(day), \(month) \(suffix)"
38 | }
39 |
40 | func daysAgo(fromDateAsString dateString: String) -> Int {
41 | guard let date = parseDate(dateString) else {
42 | return 0
43 | }
44 |
45 | return daysAgo(date)
46 | }
47 |
48 | func timeAgo(fromDateAsString dateString: String) -> String {
49 | guard var timeZone = NSTimeZone(abbreviation: "PST"), let date = NSDate(iso8601String: dateString, timeZone: &timeZone, using: nil) else {
50 | return ""
51 | }
52 |
53 | return date.shortTimeAgoSinceNow()
54 | }
55 |
56 | fileprivate func parseDate(_ dateAsString: String) -> Date? {
57 | formatter.dateFormat = "yyyy-MM-dd"
58 |
59 | guard let date = formatter.date(from: dateAsString) else {
60 | return nil
61 | }
62 |
63 | return date
64 | }
65 |
66 | fileprivate func daysAgo(_ date: Date) -> Int {
67 | return (Calendar.current as NSCalendar).components(NSCalendar.Unit.day, from: date, to: Date(), options: []).day!
68 | }
69 |
70 | fileprivate func getDayName(_ date: Date) -> String {
71 | formatter.dateFormat = "EEEE"
72 |
73 | switch daysAgo(date) {
74 | case 0 : return "Today"
75 | case 1 : return "Yesterday"
76 | default : return self.formatter.string(from: date)
77 | }
78 | }
79 |
80 | fileprivate func getMonthName(_ date: Date) -> String {
81 | formatter.dateFormat = "MMMM"
82 | return self.formatter.string(from: date)
83 | }
84 |
85 | fileprivate func getDaySuffix(_ date: Date) -> String {
86 | let day = (Calendar.current as NSCalendar).component(NSCalendar.Unit.day, from: date)
87 | let unit = units[day] ?? units[0]
88 |
89 | return "\(day)\(unit!)"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHPostSorterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostSorterTests.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/19/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import ReSwift
11 |
12 | class PHPostSorterTests: PHTestCase {
13 |
14 | func testThatFiltersSeenPosts() {
15 | let seenPost = fake.post()
16 | let unseenPost = fake.post()
17 |
18 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
19 |
20 | store.dispatch( PHMarkPostsAsSeenAction(posts: [seenPost]))
21 |
22 | let posts = PHPostSorter.filter(store, posts: [seenPost, unseenPost], by: [.Seen(true)])
23 |
24 | XCTAssertTrue(posts.count == 1)
25 | }
26 |
27 | func testThatFiltersUnseenPosts() {
28 | let seenPost = fake.post()
29 | let unseenPost = fake.post()
30 |
31 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
32 |
33 | store.dispatch( PHMarkPostsAsSeenAction(posts: [seenPost]))
34 |
35 | let posts = PHPostSorter.filter(store, posts: [seenPost, unseenPost], by: [.Seen(false)])
36 |
37 | XCTAssertTrue(posts.count == 1)
38 | }
39 |
40 | func testThatFiltersVotes() {
41 | let seenPost = fake.post(0, votes: 9, commentsCount: 0)
42 | let unseenPost = fake.post(0, votes: 15, commentsCount: 0)
43 |
44 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
45 |
46 | let posts = PHPostSorter.filter(store, posts: [seenPost, unseenPost], by: [PHPostFilter.Votes(10)])
47 |
48 | XCTAssertTrue(posts.count == 1)
49 | }
50 |
51 | func testThatSortsByVotes() {
52 | let seenPost = fake.post(0, votes: 10, commentsCount: 0)
53 | let unseenPost = fake.post(0, votes: 15, commentsCount: 0)
54 |
55 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
56 |
57 | let posts = PHPostSorter.filter(store, posts: [seenPost, unseenPost], by: [PHPostFilter.SortByVotes])
58 |
59 | XCTAssertTrue(posts.first!.votesCount == 15)
60 | }
61 |
62 | func testThatSortsPostsByUnseenAndGivenVoteCount() {
63 | let seenPost = fake.post(0, votes: 100, commentsCount: 0)
64 | let unseenPost = fake.post(0, votes: 9, commentsCount: 0)
65 | let otherUnseenPost = fake.post(0, votes: 10, commentsCount: 0)
66 |
67 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
68 |
69 | store.dispatch( PHMarkPostsAsSeenAction(posts: [seenPost]))
70 |
71 | let posts = PHPostSorter.sort(store, posts: [seenPost, unseenPost, otherUnseenPost], votes: 0)
72 |
73 | XCTAssertTrue(posts.first!.votesCount == 10)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Source/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import SwiftyTimer
11 | import Sparkle
12 | import ReSwift
13 |
14 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
15 |
16 | @NSApplicationMain
17 | class AppDelegate: NSObject, NSApplicationDelegate {
18 |
19 | @IBOutlet weak var window: NSWindow!
20 |
21 | let statusItem = NSStatusBar.system().statusItem(withLength: NSVariableStatusItemLength)
22 | let popover = NSPopover()
23 |
24 | var settingsWindow = PHPreferencesWindowController()
25 | var countdownToMarkAsSeen: Timer?
26 |
27 | fileprivate var statusBarUpdater: PHStatusBarUpdater!
28 | fileprivate var updatePostTimer: Timer?
29 | fileprivate let defaults = PHDefaults(store: store)
30 |
31 | func applicationDidFinishLaunching(_ aNotification: Notification) {
32 | if let button = statusItem.button {
33 | button.image = NSImage(named: "StatusBarButtonImage")
34 | button.imagePosition = .imageLeft
35 | button.action = #selector(togglePopover)
36 | }
37 |
38 | popover.contentViewController = PHPostListViewController(nibName: "PHPostListViewController", bundle: nil)
39 | popover.appearance = NSAppearance(named: NSAppearanceNameAqua)
40 | popover.animates = false
41 | popover.behavior = .transient
42 |
43 | PHFirstLaunchAction.perform {
44 | PHStartAtLoginAction.perform(true)
45 |
46 | SUUpdater.shared().automaticallyChecksForUpdates = true
47 | SUUpdater.shared().automaticallyDownloadsUpdates = false
48 | }
49 |
50 | statusBarUpdater = PHStatusBarUpdater(button: statusItem.button, store: store)
51 |
52 | PHLoadPostOperation.performNewer()
53 |
54 | updatePostTimer = Timer.every(10.minutes) {
55 | PHLoadPostOperation.performNewer()
56 | }
57 |
58 | SUUpdater.shared().feedURL = URL(string: kPHFeedUrl)!
59 | SUUpdater.shared().updateCheckInterval = 1.day
60 | }
61 |
62 | func applicationWillTerminate(_ aNotification: Notification) {
63 | updatePostTimer?.invalidate()
64 | updatePostTimer = nil
65 | }
66 |
67 | // MARK: Actions
68 |
69 | func togglePopover() {
70 | PHPopoverAction.toggle()
71 | }
72 |
73 | func openAppsLink() {
74 | PHOpenProductHuntAction.performWithAppsLink()
75 | }
76 |
77 | func openFAQLink() {
78 | PHOpenProductHuntAction.performWithFAQLink()
79 | }
80 |
81 | func openAboutLink() {
82 | PHOpenProductHuntAction.performWithAboutLink()
83 | }
84 |
85 | func showSettings() {
86 | PHOpenSettingsAction.perform()
87 | PHPopoverAction.close()
88 | }
89 |
90 | func quit() {
91 | NSApplication.shared().terminate(self)
92 | }
93 |
94 | func checkForUpdates() {
95 | SUUpdater.shared().checkForUpdates(nil)
96 | }
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHPostViewModelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostViewModelTests.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/18/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import ReSwift
11 |
12 | class PHPostViewModelTests: PHTestCase {
13 |
14 | func testThatRerurnsTitle() {
15 | let post = fake.post()
16 |
17 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
18 |
19 | let model = PHPostViewModel(withPost: post, store: store)
20 |
21 | XCTAssertTrue(model.title == post.title)
22 | }
23 |
24 | func testThatReturnsThumbnailUrl() {
25 | let post = fake.post()
26 |
27 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
28 |
29 | let model = PHPostViewModel(withPost: post, store: store)
30 |
31 | XCTAssertNotNil(model.thumbnailUrl)
32 | }
33 |
34 | func testThatRerurnsTagline() {
35 | let post = fake.post()
36 |
37 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
38 |
39 | let model = PHPostViewModel(withPost: post, store: store)
40 |
41 | XCTAssertTrue(model.tagline == post.tagline)
42 | }
43 |
44 | func testThatSeenIsTrueIfPostIsOlderThanOneDay() {
45 | let post = fake.post(1.days)
46 |
47 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
48 |
49 | let model = PHPostViewModel(withPost: post, store: store)
50 |
51 | store.dispatch( PHMarkPostsAsSeenAction(posts: [post]) )
52 |
53 | XCTAssertTrue(model.isSeen)
54 | }
55 |
56 | func testThatSeenIsFalseIfNotInInteractions() {
57 | let post = fake.post()
58 |
59 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
60 |
61 | let model = PHPostViewModel(withPost: post, store: store)
62 |
63 | XCTAssertFalse(model.isSeen)
64 | }
65 |
66 | func testThatSeenIsTrueIfPostIsInInteractions() {
67 | let post = fake.post()
68 |
69 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
70 |
71 | let model = PHPostViewModel(withPost: post, store: store)
72 |
73 | store.dispatch( PHMarkPostsAsSeenAction(posts: [post]) )
74 |
75 | XCTAssertTrue(model.isSeen)
76 | }
77 |
78 | func testThatReturnsVotesCount() {
79 | let post = fake.post(0.seconds, votes: 10, commentsCount: 10)
80 |
81 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
82 |
83 | let model = PHPostViewModel(withPost: post, store: store)
84 |
85 | XCTAssertTrue(model.votesCount == "10")
86 | }
87 |
88 | func testThatReturnsCommentsCount() {
89 | let post = fake.post(0.seconds, votes: 10, commentsCount: 10)
90 |
91 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
92 |
93 | let model = PHPostViewModel(withPost: post, store: store)
94 |
95 | XCTAssertTrue(model.commentsCount == "10")
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Source/Components/KPCScaleToFillNSImageView.m:
--------------------------------------------------------------------------------
1 | //
2 | // KPCScaleToFillNSImageView.m
3 | //
4 | // Created by onekiloparsec on 4/5/14.
5 | // MIT Licence
6 | //
7 |
8 | #import "KPCScaleToFillNSImageView.h"
9 |
10 | @implementation KPCScaleToFillNSImageView
11 |
12 | - (id)init
13 | {
14 | self = [super init];
15 | if (self) {
16 | [super setImageScaling:NSImageScaleAxesIndependently];
17 | }
18 | return self;
19 | }
20 |
21 | - (id)initWithCoder:(NSCoder *)aDecoder
22 | {
23 | self = [super initWithCoder:aDecoder];
24 | if (self) {
25 | [super setImageScaling:NSImageScaleAxesIndependently];
26 | }
27 | return self;
28 | }
29 |
30 | - (id)initWithFrame:(NSRect)frameRect
31 | {
32 | self = [super initWithFrame:frameRect];
33 | if (self) {
34 | [super setImageScaling:NSImageScaleAxesIndependently];
35 | }
36 | return self;
37 | }
38 |
39 | - (void)setImageScaling:(NSImageScaling)newScaling
40 | {
41 | // That's necessary to use nothing but NSImageScaleAxesIndependently
42 | [super setImageScaling:NSImageScaleAxesIndependently];
43 | }
44 |
45 | - (void)setImage:(NSImage *)image
46 | {
47 | if (image == nil) {
48 | [super setImage:image];
49 | return;
50 | }
51 |
52 | NSImage *scaleToFillImage = [NSImage imageWithSize:self.bounds.size
53 | flipped:NO
54 | drawingHandler:^BOOL(NSRect dstRect) {
55 |
56 | NSSize imageSize = [image size];
57 | NSSize imageViewSize = self.bounds.size; // Yes, do not use dstRect.
58 |
59 | NSSize newImageSize = imageSize;
60 |
61 | CGFloat imageAspectRatio = imageSize.height/imageSize.width;
62 | CGFloat imageViewAspectRatio = imageViewSize.height/imageViewSize.width;
63 |
64 | if (imageAspectRatio < imageViewAspectRatio) {
65 | // Image is more horizontal than the view. Image left and right borders need to be cropped.
66 | newImageSize.width = imageSize.height / imageViewAspectRatio;
67 | }
68 | else {
69 | // Image is more vertical than the view. Image top and bottom borders need to be cropped.
70 | newImageSize.height = imageSize.width * imageViewAspectRatio;
71 | }
72 |
73 | NSRect srcRect = NSMakeRect(imageSize.width/2.0-newImageSize.width/2.0,
74 | imageSize.height/2.0-newImageSize.height/2.0,
75 | newImageSize.width,
76 | newImageSize.height);
77 |
78 | [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
79 |
80 | [image drawInRect:dstRect // Interestingly, here needs to be dstRect and not self.bounds
81 | fromRect:srcRect
82 | operation:NSCompositeCopy
83 | fraction:1.0
84 | respectFlipped:YES
85 | hints:@{NSImageHintInterpolation: @(NSImageInterpolationHigh)}];
86 |
87 | return YES;
88 | }];
89 |
90 | [scaleToFillImage setCacheMode:NSImageCacheNever]; // Hence it will automatically redraw with new frame size of the image view.
91 |
92 | [super setImage:scaleToFillImage];
93 | }
94 |
95 | @end
96 |
--------------------------------------------------------------------------------
/Source/Controllers/PHPostListViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostListViewController.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/14/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import DateTools
11 | import SwiftyTimer
12 |
13 | class PHPostListViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate, PHDataSourceDelegate {
14 |
15 | @IBOutlet weak var productTableView: NSTableView!
16 | @IBOutlet weak var loadingView: PHLoadingView!
17 | @IBOutlet weak var lastUpdatedLabel: NSTextField!
18 | @IBOutlet weak var homeButton: PHButton!
19 | @IBOutlet weak var settingsButton: PHButton!
20 |
21 | fileprivate var source = PHPostsDataSource(store: store)
22 | fileprivate var updateUITimer: Timer?
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | view.wantsLayer = true
28 |
29 | view.layer?.backgroundColor = NSColor.ph_whiteColor().cgColor
30 |
31 | productTableView.intercellSpacing = NSSize.zero
32 |
33 | source.delegate = self
34 |
35 | loadingView.state = .loading
36 |
37 | homeButton.setImages("Icon-product-hunt", highlitedImage: "icon-kitty")
38 | }
39 |
40 | override func viewWillAppear() {
41 | super.viewWillAppear()
42 |
43 | self.source.loadNewer()
44 |
45 | PHScheduleAsSeenAction.performCancel()
46 |
47 | updateUITimer = Timer.every(15.seconds) { [weak self] in self?.updateUI() }
48 |
49 | updateUI()
50 |
51 | PHAnalitycsOperation.performTrackVisit("home")
52 | }
53 |
54 | override func viewWillDisappear() {
55 | super.viewWillDisappear()
56 |
57 | PHScheduleAsSeenAction.performSchedule()
58 |
59 | if let timer = updateUITimer {
60 | timer.invalidate()
61 | }
62 |
63 | updateUITimer = nil
64 | }
65 |
66 | func numberOfRows(in tableView: NSTableView) -> Int {
67 | return source.numberOfRows()
68 | }
69 |
70 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
71 | loadOlderIfNeeded(row)
72 | return PHSectionCell.view(tableView, owner: self, subject: source.data(atIndex: row)) ?? PHPostCell.view(tableView, owner: self, subject: source.data(atIndex: row))
73 | }
74 |
75 | func tableView(_ tableView: NSTableView, isGroupRow row: Int) -> Bool {
76 | return source.isGroup(atIndex: row)
77 | }
78 |
79 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
80 | return source.isGroup(atIndex: row) ? 45 : tableView.rowHeight
81 | }
82 |
83 | func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
84 | if let post = source.data(atIndex: row) as? PHPost {
85 | PHOpenPostOperation.perform(withPost: post)
86 | }
87 |
88 | return true
89 | }
90 |
91 | fileprivate func loadOlderIfNeeded(_ row: Int) {
92 | if source.numberOfRows()-row < 15 {
93 | source.loadOlder()
94 | }
95 | }
96 |
97 | // MARK: PHDataSourceDelegate
98 |
99 | func contentChanged() {
100 | loadingView.state = source.numberOfRows() > 0 ? .idle : .empty
101 |
102 | productTableView.reloadData()
103 |
104 | updateUI()
105 | }
106 |
107 | // MARK: Actions
108 |
109 | @IBAction func toggleProductHuntButton(_ sender: AnyObject) {
110 | PHOpenURLAction.perform(withPath: "https://producthunt.com", closeAfterLaunch: true)
111 | }
112 |
113 | @IBAction func toggleSettingsButton(_ sender: NSView) {
114 | PHOpenSettingsMenuAction.perform(sender)
115 | }
116 |
117 | func updateUI() {
118 | if let lastUpdated = (store.state.posts.lastUpdated as NSDate).timeAgoSinceNow() {
119 | lastUpdatedLabel.stringValue = "Last Updated: \(lastUpdated)"
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Product Hunt.xcodeproj/xcshareddata/xcschemes/Release.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
66 |
72 |
73 |
74 |
75 |
76 |
77 |
83 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/Product Hunt.xcodeproj/xcshareddata/xcschemes/Debug.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
43 |
44 |
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
69 |
71 |
77 |
78 |
79 |
80 |
81 |
82 |
88 |
90 |
96 |
97 |
98 |
99 |
101 |
102 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/Source/Controllers/PHAdvancedSettingsViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
33 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1070
5 | 11D50
6 | 2182
7 | 1138.32
8 | 568.00
9 |
13 |
14 | NSCustomObject
15 |
16 |
17 | com.apple.InterfaceBuilder.CocoaPlugin
18 |
19 |
23 |
24 |
27 |
30 |
33 |
36 |
37 |
93 |
94 | 0
95 | IBCocoaFramework
96 |
97 | com.apple.InterfaceBuilder.CocoaPlugin.macosx
98 |
99 |
100 | YES
101 | 3
102 | YES
103 |
104 |
105 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/readme.md:
--------------------------------------------------------------------------------
1 | When creating a sandboxed app `LSSharedFileListInsertItemURL` can no longer be used to launch the app at startup. Instead `SMLoginItemSetEnabled` should be used with a helper app inside the main app's bundle that launches the main app. **LaunchAtLoginHelper** is that helper app, it is designed to be as easy as possible to integrate into the main app to allow it to launch at login when sandboxed.
2 |
3 | A lot of research was put into this helper app. For example [Apple's docs](http://developer.apple.com/library/mac/#documentation/Security/Conceptual/AppSandboxDesignGuide/DesigningYourSandbox/DesigningYourSandbox.html#//apple_ref/doc/uid/TP40011183-CH4-SW3) state that `LSRegisterURL` should be used to register the helper app, however this never seemed to work and after further digging it turns out this is a [typo in the docs](https://devforums.apple.com/message/647212#647212). Many examples I found online used `NSWorkspace launchApplication:` to launch the main app, however this was blocked by sandboxing so a url scheme is used instead.
4 |
5 | **LaunchAtLoginHelper** calls the main app's scheme twice, once to launchg the app and then again with `launchedAtLogin` so the main app can know if it has been launched at login. For example [Play by Play](http://playbyplayapp.com) uses this to hide the app if it was launched at login.
6 |
7 | This project contains a [sample app](https://github.com/kgn/LaunchAtLoginHelper/tree/master/LaunchAtLoginSample) to demonstrate how the main app should be configured and how to setup a checkbox to enable and disable launching at login.
8 |
9 | # How to use
10 |
11 | First download or even better submodule **LaunchAtLoginHelper**. To clone the repository as a submodule use the following commands:
12 |
13 | ```
14 | $ cd
15 | $ git clone --recursive https://github.com/kgn/LaunchAtLoginHelper.git
16 | ```
17 |
18 | **LaunchAtLoginHelper** uses a url scheme to launch the main app, if the main app doesn't have a url scheme yet add one.
19 |
20 | 
21 |
22 | There are two files missing from this repo that are specific to your instance of the helper app. To generate these files run the `setup.py` python script and pass in the url scheme to launch the main app and the bundle identifier of the helper app, this is usually based of of the bundle identifier of the main app but with *Helper* added onto the end.
23 |
24 | ```
25 | $ cd LaunchAtLoginHelper
26 | $ python setup.py
27 | ```
28 |
29 | For the sample code the above will look like this:
30 |
31 | ```
32 | $ cd LaunchAtLoginHelper
33 | $ python setup.py launchatloginsample com.InScopeApps.ShellTo.LaunchAtLoginHelper
34 | ```
35 |
36 | This will create `LLStrings.h` which is used in both the helper app and the main app and contains `#define`'s for the url scheme and the helper app's bundle identifier. `LaunchAtLoginHelper-Info.plist` is also created for the helper app with it's custom bundle identifier filled in.
37 |
38 | Once these two files are generated it's time to add **LaunchAtLoginHelper** to the main app. Drag `LaunchAtLoginHelper.xcodeproj`, `LLStrings.h`, `LLManager.h`, and `LLManager.m` to the main app's project.
39 |
40 | 
41 |
42 | Next go to the *Build Phases* for the main app and add **LaunchAtLoginHelper** as a *Target Dependency* and create a new *Copy Files Build Phase*. Set the *Destination* of this build phase to `Wrapper` and the *Subpath* to `Contents/Library/LoginItems`.
43 |
44 | 
45 |
46 | Lastly add `ServiceManagement.framework` to the main app.
47 |
48 | Once this is done use `LLManager` to enable and disable launching at login! [**LaunchAtLoginSample**](https://github.com/kgn/LaunchAtLoginHelper/blob/master/LaunchAtLoginSample/LLAppDelegate.m) shows how to hook this up to a checkbox.
49 |
50 | ``` obj-c
51 | #import "LLManager.h"
52 |
53 | [LLManager launchAtLogin] // will the app launch at login?
54 | [LLManager setLaunchAtLogin:YES] // set the app to launch at login
55 | ```
56 |
57 | # Bindings
58 |
59 | The `LLManager` class supports KVO and Cocoa Bindings. This allows for a completely code-free implementaion of this class. To get started, open the Interface Builder document in which you plan to create a login toggle. Drag a generic `NSObject` from the Utilities pane, and drop it onto your canvas. Select the newly created object, and open the Identity inspector tab in the Utilities pane. Change the class from `NSObject` to `LLManager`. Now, select to your login toggle (checkbox) and open the Bindings inspector in the Utilities pane. Expand `Value`, check the "Bind to", and select the name of the `LLManager` object you created earlier. Set the key path to `self.launchAtLogin`. You're done!
60 |
61 | ---
62 |
63 | Special thanks to [Curtis Hard](http://www.geekygoodness.com) for offering some much needed advice on this project.
--------------------------------------------------------------------------------
/Source/Tests/UnitTests/PHPostDataSourceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostDataSourceTests.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/31/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import ReSwift
11 |
12 | class PHPostDataSourceTests: PHTestCase {
13 |
14 | override func tearDown() {
15 | super.tearDown()
16 | }
17 |
18 | func testReturnsCountOfRows() {
19 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 0, "search[category]": "all"], response: ["posts" : [fake.post().description()]], error: nil)
20 |
21 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
22 |
23 | store.dispatch( PHTokenGetAction(token: fake.token() ))
24 |
25 | let source = PHPostsDataSource(store: store)
26 |
27 | source.loadNewer()
28 |
29 | XCTAssertTrue(source.numberOfRows() == 2)
30 | }
31 |
32 | func testThatReturnsSectionAtIndex() {
33 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 0, "search[category]": "all"], response: ["posts" : [fake.post().description()]], error: nil)
34 |
35 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
36 |
37 | store.dispatch( PHTokenGetAction(token: fake.token() ))
38 |
39 | let source = PHPostsDataSource(store: store)
40 |
41 | source.loadNewer()
42 |
43 | XCTAssertNotNil(source.data(atIndex: 0) as? String)
44 | }
45 |
46 | func testThatReturnsPostAtIndex() {
47 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 0, "search[category]": "all"], response: ["posts" : [fake.post().description()]], error: nil)
48 |
49 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
50 |
51 | store.dispatch( PHTokenGetAction(token: fake.token() ))
52 |
53 | let source = PHPostsDataSource(store: store)
54 |
55 | source.loadNewer()
56 |
57 | XCTAssertNotNil(source.data(atIndex: 1) as? PHPost)
58 | }
59 |
60 | func testThatRerurnsTrueIfSectionIsGroup() {
61 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 0, "search[category]": "all"], response: ["posts" : [fake.post().description()]], error: nil)
62 |
63 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
64 |
65 | store.dispatch( PHTokenGetAction(token: fake.token() ))
66 |
67 | let source = PHPostsDataSource(store: store)
68 |
69 | source.loadNewer()
70 |
71 | XCTAssertTrue(source.isGroup(atIndex: 0))
72 | XCTAssertFalse(source.isGroup(atIndex: 1))
73 | }
74 |
75 | func testLoadsNewer() {
76 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 0, "search[category]": "all"], response: ["posts" : [fake.post().description()]], error: nil)
77 |
78 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
79 |
80 | store.dispatch( PHTokenGetAction(token: fake.token() ))
81 |
82 | let source = PHPostsDataSource(store: store)
83 |
84 | source.loadNewer()
85 |
86 | XCTAssertEqual(source.numberOfRows(), 2)
87 | }
88 |
89 | func testLoadsOlder() {
90 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 0, "search[category]": "all"], response: ["posts" : [fake.post().description()]], error: nil)
91 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 1, "search[category]": "all"], response: ["posts" : [fake.post(-1.day, votes: 10, commentsCount: 0).description()]], error: nil)
92 |
93 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
94 |
95 | store.dispatch( PHTokenGetAction(token: fake.token() ))
96 |
97 | let source = PHPostsDataSource(store: store)
98 |
99 | source.loadNewer()
100 | source.loadOlder()
101 |
102 | XCTAssertEqual(source.numberOfRows(), 4)
103 | }
104 |
105 | func testThatFiltersPostsWithGivenVoteCount() {
106 | let firstPost = fake.post(0, votes: 51, commentsCount: 0)
107 | let secondPosts = fake.post(0, votes: 25, commentsCount: 0)
108 |
109 | endpoint.addFake("GET", url: "posts", parameters: ["days_ago": 0, "search[category]": "all"], response: ["posts" : [firstPost.description(), secondPosts.description()]], error: nil)
110 |
111 | let store = Store(reducer: PHAppReducer(), state: nil, middleware: [PHTrackingMiddleware])
112 |
113 | store.dispatch( PHTokenGetAction(token: fake.token() ))
114 |
115 | store.dispatch( PHSettngsActionFilterCount(filterCount: 50) )
116 |
117 | let source = PHPostsDataSource(store: store)
118 |
119 | source.loadNewer()
120 |
121 | XCTAssertEqual(source.numberOfRows(), 2)
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Source/Views/PHPostCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHPostCell.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/15/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Kingfisher
11 |
12 | class PHPostCell: NSTableCellView {
13 |
14 | @IBOutlet weak var thumbnailImageView: KPCScaleToFillNSImageView!
15 | @IBOutlet weak var titleLabel: NSTextField!
16 | @IBOutlet weak var taglineLabel: NSTextField!
17 | @IBOutlet weak var seenView: PHSeenView!
18 | @IBOutlet weak var voteImageView: NSImageView!
19 | @IBOutlet weak var voteCountLabel: NSTextField!
20 | @IBOutlet weak var commentImageView: NSImageView!
21 | @IBOutlet weak var commentsCountLabel: NSTextField!
22 | @IBOutlet weak var timeAgoLabel: NSTextField!
23 | @IBOutlet weak var twitterButton: PHButton!
24 | @IBOutlet weak var facebookButton: PHButton!
25 |
26 | fileprivate let cursor = NSCursor.pointingHand()
27 | fileprivate var model: PHPostViewModel?
28 | fileprivate var post: PHPost?
29 | fileprivate var trackingArea: NSTrackingArea?
30 | fileprivate var mouseInside = false {
31 | didSet {
32 | updateUI()
33 | }
34 | }
35 |
36 | class func view(_ tableView: NSTableView, owner: AnyObject?, subject: AnyObject?) -> NSView {
37 | let view = tableView.make(withIdentifier: "postCellIdentifier", owner: owner) as! PHPostCell
38 |
39 | if let post = subject as? PHPost {
40 | view.setPost(post)
41 | }
42 |
43 | return view
44 | }
45 |
46 | override func awakeFromNib() {
47 | super.awakeFromNib()
48 | commonInit()
49 | }
50 |
51 | override func prepareForReuse() {
52 | super.prepareForReuse()
53 | trackingArea = nil
54 | mouseInside = false
55 | twitterButton.resetTrackingArea()
56 | }
57 |
58 | override func resetCursorRects() {
59 | addCursorRect(bounds, cursor: cursor)
60 | cursor.set()
61 | }
62 |
63 | fileprivate func commonInit() {
64 | wantsLayer = true
65 |
66 | thumbnailImageView.wantsLayer = true
67 | thumbnailImageView.layer?.masksToBounds = true
68 | thumbnailImageView.layer?.cornerRadius = 3
69 |
70 | twitterButton.setImages("icon-twitter", highlitedImage: "icon-twitter-hovered")
71 | facebookButton.setImages("icon-facebook", highlitedImage: "icon-facebook-hovered")
72 |
73 | taglineLabel.textColor = NSColor.ph_grayColor()
74 | voteCountLabel.textColor = NSColor.ph_grayColor()
75 | commentsCountLabel.textColor = NSColor.ph_grayColor()
76 | timeAgoLabel.textColor = NSColor.ph_grayColor()
77 |
78 | voteImageView.image = NSImage(named: "icon-upvote")!.tintedImageWithColor(NSColor.ph_grayColor())
79 | commentImageView.image = NSImage(named: "comment-icon")!.tintedImageWithColor(NSColor.ph_grayColor())
80 | }
81 |
82 | fileprivate func setPost(_ post: PHPost?) {
83 | self.post = post
84 |
85 | guard let post = post else {
86 | return
87 | }
88 |
89 | model = PHPostViewModel(withPost: post, store: store)
90 | updateUI()
91 | }
92 |
93 | fileprivate func updateUI() {
94 | guard let model = model else {
95 | return
96 | }
97 |
98 | layer?.backgroundColor = mouseInside ? NSColor.ph_highlightColor().cgColor : NSColor.ph_whiteColor().cgColor
99 |
100 | seenView.isHidden = model.isSeen
101 |
102 | titleLabel.stringValue = model.title
103 | taglineLabel.stringValue = model.tagline
104 | voteCountLabel.stringValue = model.votesCount
105 | commentsCountLabel.stringValue = model.commentsCount
106 | timeAgoLabel.stringValue = model.createdAt
107 |
108 | thumbnailImageView.kf.setImage(with: model.thumbnailUrl, placeholder: NSImage(named: "placeholder"), options: nil, progressBlock: nil, completionHandler: nil)
109 |
110 | twitterButton.isHidden = !mouseInside
111 | facebookButton.isHidden = !mouseInside
112 | }
113 |
114 | fileprivate func createTrackingAreaIfNeeded() {
115 | if trackingArea == nil {
116 | trackingArea = NSTrackingArea(rect: CGRect.zero, options: [NSTrackingAreaOptions.inVisibleRect, NSTrackingAreaOptions.mouseEnteredAndExited, NSTrackingAreaOptions.activeAlways], owner: self, userInfo: nil)
117 | }
118 | }
119 |
120 | override func updateTrackingAreas() {
121 | super.updateTrackingAreas()
122 |
123 | createTrackingAreaIfNeeded()
124 |
125 | if !trackingAreas.contains(trackingArea!) {
126 | addTrackingArea(trackingArea!)
127 | }
128 | }
129 |
130 | override func mouseEntered(with theEvent: NSEvent) {
131 | mouseInside = true
132 | }
133 |
134 | override func mouseExited(with theEvent: NSEvent) {
135 | mouseInside = false
136 | }
137 |
138 | @IBAction func toggleTwitterShare(_ sender: AnyObject) {
139 | PHShareAction.sharedInstance.performTwitter(post)
140 | }
141 |
142 | @IBAction func toggleFacebookShare(_ sender: AnyObject) {
143 | PHShareAction.sharedInstance.performFacebook(post)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Source/Models/PHDefaults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHDefaults.swift
3 | // Product Hunt
4 | //
5 | // Created by Vlado on 4/28/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ISO8601
11 | import ReSwift
12 |
13 | class PHDefaults: StoreSubscriber {
14 |
15 | fileprivate let kAutoLoginKey = "autoLoginKey"
16 | fileprivate let kShowCountKey = "showBadgeCount"
17 | fileprivate let kFilterCountKey = "filterPostsCount"
18 |
19 | fileprivate let kSeenPostsKey = "seenPostsKey"
20 | fileprivate let kSeenPostsKeyDate = "seenPostsDate"
21 | fileprivate let kSeenPostsKeyIds = "seenPostsDateIds"
22 |
23 | fileprivate let kTokenKey = "oauthToken"
24 | fileprivate let kTokenKeyAccess = "accessToken"
25 |
26 | fileprivate var store: Store
27 |
28 | init(store: Store) {
29 | self.store = store
30 |
31 | migrate()
32 |
33 | store.dispatch( PHSettingsSetAction(settings: readSettings()) )
34 | store.dispatch( PHSeenPostsSetAction(seenPost: readSeenPosts()) )
35 | store.dispatch( PHTokenGetAction(token: readToken()) )
36 |
37 | store.subscribe(self)
38 | }
39 |
40 | deinit {
41 | store.unsubscribe(self)
42 | }
43 |
44 | func newState(state: PHAppState) {
45 | set(state.settings.autologinEnabled, key: kAutoLoginKey)
46 | set(state.settings.showsCount, key: kShowCountKey)
47 | set(state.settings.filterCount, key: kFilterCountKey)
48 |
49 | set([ kSeenPostsKeyDate: state.seenPosts.date, kSeenPostsKeyIds: Array(state.seenPosts.postIds) ], key: kSeenPostsKey)
50 |
51 | set([kTokenKeyAccess: state.token.accessToken], key: kTokenKey)
52 | }
53 |
54 | fileprivate func readSettings() -> PHSettings {
55 | let autologinEnabled = get(kAutoLoginKey, objectType: Bool()) ?? true
56 | let showCountKey = get(kShowCountKey, objectType: Bool()) ?? true
57 | let filterCount = get(kFilterCountKey, objectType: Int()) ?? 10
58 |
59 | return PHSettings(autologinEnabled: autologinEnabled, showsCount: showCountKey, filterCount: filterCount)
60 | }
61 |
62 | fileprivate func readSeenPosts() -> PHSeenPosts {
63 | let data = get( kSeenPostsKey, objectType: [String : AnyObject]() ) ?? [String : AnyObject]()
64 |
65 | var day = Date()
66 | var ids = Set()
67 |
68 | if let date = data[kSeenPostsKeyDate] as? Date {
69 | day = date
70 | }
71 |
72 | if let postIds = data[kSeenPostsKeyIds] as? [Int] {
73 | ids = Set(postIds)
74 | }
75 |
76 | return PHSeenPosts(date: day, postIds: ids)
77 | }
78 |
79 | fileprivate func readToken() -> PHToken {
80 | let data = get(kTokenKey, objectType: [String : AnyObject]()) ?? [String : AnyObject]()
81 |
82 | var access = ""
83 |
84 | if let accessToken = data[kTokenKeyAccess] as? String {
85 | access = accessToken
86 | }
87 |
88 | return PHToken(accessToken: access)
89 | }
90 |
91 | // TODO: Remove migrate in V1.1
92 | fileprivate func migrate() {
93 | let deprecatedTokenKey = "authToken"
94 | let deprecatedShowsCountKey = "showsCount"
95 | let deprecatedFilterCountKey = "filterCount"
96 | let deprecatedAutoLoginKey = "autoLogin"
97 | let deprecatedSeenPostsKey = "seenPosts"
98 | let deprecatedLastUpdatedKey = "lastUpdatedDate"
99 |
100 | if let tokenData = get(deprecatedTokenKey, objectType: [String : Any]()), let token = PHToken.token(fromDictionary: tokenData) {
101 | set([kTokenKeyAccess: token.accessToken], key: kTokenKey)
102 | }
103 |
104 | if let showsCount = get(deprecatedShowsCountKey, objectType: Bool()) {
105 | set(showsCount as AnyObject, key: kShowCountKey)
106 | }
107 |
108 | if let filterCount = get(deprecatedFilterCountKey, objectType: Int()) {
109 | set(filterCount as AnyObject, key: kFilterCountKey)
110 | }
111 |
112 | if let autologin = get(deprecatedAutoLoginKey, objectType: Bool()) {
113 | set(autologin as AnyObject, key: kAutoLoginKey)
114 | }
115 |
116 | if let seenPosts = get(deprecatedSeenPostsKey, objectType: [String : [Int]]()), let key = seenPosts.keys.first, let date = NSDate(iso8601String: key), let ids = seenPosts[key] {
117 | set([ kSeenPostsKeyDate: date, kSeenPostsKeyIds: ids ], key: kSeenPostsKey)
118 | }
119 |
120 | remove(deprecatedTokenKey)
121 | remove(deprecatedShowsCountKey)
122 | remove(deprecatedFilterCountKey)
123 | remove(deprecatedAutoLoginKey)
124 | remove(deprecatedSeenPostsKey)
125 | remove(deprecatedLastUpdatedKey)
126 | }
127 |
128 | fileprivate func set(_ object: Any, key: String) {
129 | let defaults = UserDefaults.standard
130 | defaults.set(object, forKey: key)
131 | defaults.synchronize()
132 | }
133 |
134 | fileprivate func get(_ key: String, objectType: T) -> T? {
135 | guard let object = UserDefaults.standard.object(forKey: key) as? T else {
136 | return nil
137 | }
138 |
139 | return object
140 | }
141 |
142 | fileprivate func remove(_ key: String) {
143 | let defaults = UserDefaults.standard
144 | defaults.removeObject(forKey: key)
145 | defaults.synchronize()
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Source/Components/PHSettings/PHPreferencesWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PHTestSettingsViewController.swift
3 | // ProductHunt
4 | //
5 | // Created by Vlado on 3/22/16.
6 | // Copyright © 2016 ProductHunt. All rights reserved.
7 | //
8 | // Taken from https://github.com/phranck/CCNPreferencesWindowController
9 |
10 | import AppKit
11 |
12 | let PHPreferencesToolbarIdentifier = "PreferencesMainToolbar"
13 | let PHPreferencesWindowFrameAutoSaveName = "PreferencesWindowFrameAutoSaveName"
14 | let PHPreferencesDefaultWindowRect = NSMakeRect(0, 0, 420, 230)
15 | let escapeKey = 53
16 |
17 | class PHPreferencesWindowController : NSWindowController, NSToolbarDelegate, NSWindowDelegate {
18 |
19 | fileprivate var toolbar: NSToolbar?
20 | fileprivate var toolbarDefaultItemIdentifiers = [String]()
21 | fileprivate var activeViewController: PHPreferencesWindowControllerProtocol?
22 |
23 | var viewControllers = [PHPreferencesWindowControllerProtocol]() {
24 | didSet {
25 | setupToolbar()
26 | }
27 | }
28 |
29 | init() {
30 | let masks: NSWindowStyleMask = [.closable, .miniaturizable, .resizable, .fullScreen, .fullScreen]
31 | let window = PHPreferencesWindow(contentRect: PHPreferencesDefaultWindowRect, styleMask: masks, backing: .buffered, defer: true)
32 |
33 | window.isMovableByWindowBackground = true
34 |
35 | super.init(window: window)
36 | }
37 |
38 | required init?(coder: NSCoder) {
39 | super.init(coder: coder)
40 | }
41 |
42 | func showPreferencesWindow() {
43 | guard let window = window, !window.isVisible else {
44 | return
45 | }
46 |
47 | showWindow(self)
48 | window.makeKeyAndOrderFront(self)
49 | NSApplication.shared().activate(ignoringOtherApps: true)
50 |
51 | if activeViewController == nil {
52 | activateViewController(viewControllers[0], animate:false)
53 | window.center()
54 | }
55 |
56 | if let toolbar = window.toolbar, toolbarDefaultItemIdentifiers.count > 0 {
57 | toolbar.selectedItemIdentifier = toolbarDefaultItemIdentifiers.first
58 | }
59 | }
60 |
61 | func dismissPreferencesWindow() {
62 | close()
63 | }
64 |
65 | fileprivate func setupToolbar() {
66 | guard let window = window else {
67 | return
68 | }
69 |
70 | window.toolbar = nil
71 | toolbar = nil
72 | toolbarDefaultItemIdentifiers.removeAll()
73 |
74 | if viewControllers.count > 0 {
75 | toolbar = NSToolbar(identifier: PHPreferencesToolbarIdentifier)
76 |
77 | toolbar?.allowsUserCustomization = true
78 | toolbar?.autosavesConfiguration = true
79 |
80 | toolbar?.delegate = self
81 | window.toolbar = toolbar
82 | }
83 | }
84 |
85 | fileprivate func activateViewController(_ viewController: PHPreferencesWindowControllerProtocol, animate: Bool) {
86 | if let preferencesViewController = viewController as? NSViewController {
87 |
88 | let viewControllerFrame = preferencesViewController.view.frame
89 |
90 | if let currentWindowFrame = window?.frame,
91 | let frameRectForContentRect = window?.frameRect(forContentRect: viewControllerFrame) {
92 |
93 | let deltaY = NSHeight(currentWindowFrame) - NSHeight(frameRectForContentRect)
94 | let newWindowFrame = NSMakeRect(NSMinX(currentWindowFrame), NSMinY(currentWindowFrame) + deltaY, NSWidth(frameRectForContentRect), NSHeight(frameRectForContentRect))
95 |
96 | window?.title = viewController.preferencesTitle() as String
97 |
98 | let newView = preferencesViewController.view
99 | newView.frame.origin = NSMakePoint(0, 0)
100 | newView.alphaValue = 0.0
101 | newView.autoresizingMask = NSAutoresizingMaskOptions()
102 |
103 | if let previousViewController = activeViewController as? NSViewController {
104 | previousViewController.view.removeFromSuperview()
105 | }
106 |
107 | window?.contentView!.addSubview(newView)
108 |
109 | if let firstResponder = viewController.firstResponder?() {
110 | window?.makeFirstResponder(firstResponder)
111 | }
112 |
113 | NSAnimationContext.runAnimationGroup({
114 | (context: NSAnimationContext) -> Void in
115 | context.duration = (animate ? 0.25 : 0.0)
116 | context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
117 | self.window?.animator().setFrame(newWindowFrame, display: true)
118 | newView.animator().alphaValue = 1.0
119 | }) {
120 | () -> Void in
121 | self.activeViewController = viewController
122 | }
123 | }
124 | }
125 | }
126 |
127 | fileprivate func viewControllerWithIdentifier(_ identifier: NSString) -> PHPreferencesWindowControllerProtocol? {
128 | for viewController in viewControllers {
129 | if viewController.preferencesIdentifier() == identifier as String {
130 | return viewController
131 | }
132 | }
133 |
134 | return nil
135 | }
136 |
137 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: String, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
138 | guard let viewController = viewControllerWithIdentifier(itemIdentifier as NSString) else {
139 | return nil
140 | }
141 |
142 | let identifier = viewController.preferencesIdentifier()
143 | let label = viewController.preferencesTitle()
144 | let icon = viewController.preferencesIcon()
145 |
146 | let toolbarItem = NSToolbarItem(itemIdentifier: identifier as String)
147 | toolbarItem.label = label
148 | toolbarItem.paletteLabel = label
149 | toolbarItem.image = icon
150 | if let tooltip = viewController.preferencesToolTip?() {
151 | toolbarItem.toolTip = tooltip
152 | }
153 | toolbarItem.target = self
154 | toolbarItem.action = #selector(PHPreferencesWindowController.toolbarItemAction(_:))
155 |
156 | return toolbarItem
157 | }
158 |
159 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
160 | var identifiers = [String]()
161 |
162 | for viewController in viewControllers {
163 | identifiers.append(viewController.preferencesIdentifier())
164 | }
165 |
166 | toolbarDefaultItemIdentifiers = identifiers
167 |
168 | return identifiers
169 | }
170 |
171 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
172 | return toolbarDefaultItemIdentifiers(toolbar)
173 | }
174 |
175 | func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [String] {
176 | return toolbarDefaultItemIdentifiers(toolbar)
177 | }
178 |
179 | func toolbarItemAction(_ toolbarItem: NSToolbarItem) {
180 | guard let activeViewController = activeViewController, activeViewController.preferencesIdentifier() != toolbarItem.itemIdentifier else {
181 | return
182 | }
183 |
184 | if let viewController = viewControllerWithIdentifier(toolbarItem.itemIdentifier as NSString) {
185 | activateViewController(viewController, animate: true)
186 | }
187 | }
188 | }
189 |
190 | class PHPreferencesWindow : NSWindow {
191 |
192 | override init(contentRect: NSRect, styleMask aStyle: NSWindowStyleMask, backing bufferingType: NSBackingStoreType, defer flag: Bool) {
193 | super.init(contentRect: contentRect, styleMask: aStyle, backing: bufferingType, defer: flag)
194 | commonInit()
195 | }
196 |
197 | fileprivate func commonInit() {
198 | center()
199 |
200 | setFrameAutosaveName(PHPreferencesWindowFrameAutoSaveName)
201 | setFrameFrom(PHPreferencesWindowFrameAutoSaveName)
202 | }
203 |
204 | override func keyDown(with theEvent: NSEvent) {
205 | switch Int(theEvent.keyCode) {
206 |
207 | case escapeKey:
208 | orderOut(nil)
209 | close()
210 |
211 | default:
212 | super.keyDown(with: theEvent)
213 | }
214 | }
215 |
216 | }
217 |
--------------------------------------------------------------------------------
/Source/Controllers/PHGeneralSettingsViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
36 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/LaunchAtLoginHelper/LaunchAtLoginHelper.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 734C379915414CE200994189 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 734C379815414CE200994189 /* Cocoa.framework */; };
11 | 73C920E915414E7800C2A75A /* LLHAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 73C920E615414E7800C2A75A /* LLHAppDelegate.m */; };
12 | 73C920EA15414E7800C2A75A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 73C920E715414E7800C2A75A /* main.m */; };
13 | 73C920EB15414E7800C2A75A /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 73C920E815414E7800C2A75A /* MainMenu.xib */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | 734C379415414CE200994189 /* LaunchAtLoginHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LaunchAtLoginHelper.app; sourceTree = BUILT_PRODUCTS_DIR; };
18 | 734C379815414CE200994189 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
19 | 734C379B15414CE200994189 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
20 | 734C379D15414CE200994189 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
21 | 73C920E515414E7800C2A75A /* LLHAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LLHAppDelegate.h; path = LaunchAtLoginHelper/LLHAppDelegate.h; sourceTree = ""; };
22 | 73C920E615414E7800C2A75A /* LLHAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LLHAppDelegate.m; path = LaunchAtLoginHelper/LLHAppDelegate.m; sourceTree = ""; };
23 | 73C920E715414E7800C2A75A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = LaunchAtLoginHelper/main.m; sourceTree = ""; };
24 | 73C920E815414E7800C2A75A /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MainMenu.xib; path = LaunchAtLoginHelper/MainMenu.xib; sourceTree = ""; };
25 | 73C920FB15414FC900C2A75A /* LaunchAtLoginHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = LaunchAtLoginHelper.entitlements; path = LaunchAtLoginHelper/LaunchAtLoginHelper.entitlements; sourceTree = ""; };
26 | E782819B1CCFC3FB00447D45 /* LaunchAtLoginHelper-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "LaunchAtLoginHelper-Info.plist"; path = "LaunchAtLoginHelper/LaunchAtLoginHelper-Info.plist"; sourceTree = ""; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFrameworksBuildPhase section */
30 | 734C379115414CE200994189 /* Frameworks */ = {
31 | isa = PBXFrameworksBuildPhase;
32 | buildActionMask = 2147483647;
33 | files = (
34 | 734C379915414CE200994189 /* Cocoa.framework in Frameworks */,
35 | );
36 | runOnlyForDeploymentPostprocessing = 0;
37 | };
38 | /* End PBXFrameworksBuildPhase section */
39 |
40 | /* Begin PBXGroup section */
41 | 734C378915414CE200994189 = {
42 | isa = PBXGroup;
43 | children = (
44 | E782819B1CCFC3FB00447D45 /* LaunchAtLoginHelper-Info.plist */,
45 | 73C920E715414E7800C2A75A /* main.m */,
46 | 73C920E515414E7800C2A75A /* LLHAppDelegate.h */,
47 | 73C920E615414E7800C2A75A /* LLHAppDelegate.m */,
48 | 73C920E815414E7800C2A75A /* MainMenu.xib */,
49 | 73C920FB15414FC900C2A75A /* LaunchAtLoginHelper.entitlements */,
50 | 734C379715414CE200994189 /* Frameworks */,
51 | 734C379515414CE200994189 /* Products */,
52 | );
53 | sourceTree = "";
54 | usesTabs = 0;
55 | };
56 | 734C379515414CE200994189 /* Products */ = {
57 | isa = PBXGroup;
58 | children = (
59 | 734C379415414CE200994189 /* LaunchAtLoginHelper.app */,
60 | );
61 | name = Products;
62 | sourceTree = "";
63 | };
64 | 734C379715414CE200994189 /* Frameworks */ = {
65 | isa = PBXGroup;
66 | children = (
67 | 734C379B15414CE200994189 /* AppKit.framework */,
68 | 734C379D15414CE200994189 /* Foundation.framework */,
69 | 734C379815414CE200994189 /* Cocoa.framework */,
70 | );
71 | name = Frameworks;
72 | sourceTree = "";
73 | };
74 | /* End PBXGroup section */
75 |
76 | /* Begin PBXNativeTarget section */
77 | 734C379315414CE200994189 /* LaunchAtLoginHelper */ = {
78 | isa = PBXNativeTarget;
79 | buildConfigurationList = 734C37B215414CE200994189 /* Build configuration list for PBXNativeTarget "LaunchAtLoginHelper" */;
80 | buildPhases = (
81 | 734C379015414CE200994189 /* Sources */,
82 | 734C379115414CE200994189 /* Frameworks */,
83 | 734C379215414CE200994189 /* Resources */,
84 | );
85 | buildRules = (
86 | );
87 | dependencies = (
88 | );
89 | name = LaunchAtLoginHelper;
90 | productName = LaunchAtLoginHelper;
91 | productReference = 734C379415414CE200994189 /* LaunchAtLoginHelper.app */;
92 | productType = "com.apple.product-type.application";
93 | };
94 | /* End PBXNativeTarget section */
95 |
96 | /* Begin PBXProject section */
97 | 734C378B15414CE200994189 /* Project object */ = {
98 | isa = PBXProject;
99 | attributes = {
100 | CLASSPREFIX = LLH;
101 | LastUpgradeCheck = 0810;
102 | ORGANIZATIONNAME = "David Keegan";
103 | TargetAttributes = {
104 | 734C379315414CE200994189 = {
105 | DevelopmentTeam = MGTRPU24M6;
106 | SystemCapabilities = {
107 | com.apple.Sandbox = {
108 | enabled = 0;
109 | };
110 | };
111 | };
112 | };
113 | };
114 | buildConfigurationList = 734C378E15414CE200994189 /* Build configuration list for PBXProject "LaunchAtLoginHelper" */;
115 | compatibilityVersion = "Xcode 3.2";
116 | developmentRegion = English;
117 | hasScannedForEncodings = 0;
118 | knownRegions = (
119 | en,
120 | );
121 | mainGroup = 734C378915414CE200994189;
122 | productRefGroup = 734C379515414CE200994189 /* Products */;
123 | projectDirPath = "";
124 | projectRoot = "";
125 | targets = (
126 | 734C379315414CE200994189 /* LaunchAtLoginHelper */,
127 | );
128 | };
129 | /* End PBXProject section */
130 |
131 | /* Begin PBXResourcesBuildPhase section */
132 | 734C379215414CE200994189 /* Resources */ = {
133 | isa = PBXResourcesBuildPhase;
134 | buildActionMask = 2147483647;
135 | files = (
136 | 73C920EB15414E7800C2A75A /* MainMenu.xib in Resources */,
137 | );
138 | runOnlyForDeploymentPostprocessing = 0;
139 | };
140 | /* End PBXResourcesBuildPhase section */
141 |
142 | /* Begin PBXSourcesBuildPhase section */
143 | 734C379015414CE200994189 /* Sources */ = {
144 | isa = PBXSourcesBuildPhase;
145 | buildActionMask = 2147483647;
146 | files = (
147 | 73C920E915414E7800C2A75A /* LLHAppDelegate.m in Sources */,
148 | 73C920EA15414E7800C2A75A /* main.m in Sources */,
149 | );
150 | runOnlyForDeploymentPostprocessing = 0;
151 | };
152 | /* End PBXSourcesBuildPhase section */
153 |
154 | /* Begin XCBuildConfiguration section */
155 | 734C37B015414CE200994189 /* Debug */ = {
156 | isa = XCBuildConfiguration;
157 | buildSettings = {
158 | ALWAYS_SEARCH_USER_PATHS = NO;
159 | CLANG_WARN_BOOL_CONVERSION = YES;
160 | CLANG_WARN_CONSTANT_CONVERSION = YES;
161 | CLANG_WARN_EMPTY_BODY = YES;
162 | CLANG_WARN_ENUM_CONVERSION = YES;
163 | CLANG_WARN_INFINITE_RECURSION = YES;
164 | CLANG_WARN_INT_CONVERSION = YES;
165 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
166 | CLANG_WARN_UNREACHABLE_CODE = YES;
167 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
168 | COMBINE_HIDPI_IMAGES = YES;
169 | COPY_PHASE_STRIP = NO;
170 | ENABLE_STRICT_OBJC_MSGSEND = YES;
171 | ENABLE_TESTABILITY = YES;
172 | GCC_C_LANGUAGE_STANDARD = gnu99;
173 | GCC_DYNAMIC_NO_PIC = NO;
174 | GCC_ENABLE_OBJC_EXCEPTIONS = YES;
175 | GCC_NO_COMMON_BLOCKS = YES;
176 | GCC_OPTIMIZATION_LEVEL = 0;
177 | GCC_PREPROCESSOR_DEFINITIONS = (
178 | "DEBUG=1",
179 | "$(inherited)",
180 | );
181 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
182 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
184 | GCC_WARN_ABOUT_RETURN_TYPE = YES;
185 | GCC_WARN_UNDECLARED_SELECTOR = YES;
186 | GCC_WARN_UNINITIALIZED_AUTOS = YES;
187 | GCC_WARN_UNUSED_FUNCTION = YES;
188 | GCC_WARN_UNUSED_VARIABLE = YES;
189 | MACOSX_DEPLOYMENT_TARGET = 10.6;
190 | ONLY_ACTIVE_ARCH = YES;
191 | SDKROOT = macosx;
192 | SKIP_INSTALL = YES;
193 | };
194 | name = Debug;
195 | };
196 | 734C37B115414CE200994189 /* Release */ = {
197 | isa = XCBuildConfiguration;
198 | buildSettings = {
199 | ALWAYS_SEARCH_USER_PATHS = NO;
200 | CLANG_WARN_BOOL_CONVERSION = YES;
201 | CLANG_WARN_CONSTANT_CONVERSION = YES;
202 | CLANG_WARN_EMPTY_BODY = YES;
203 | CLANG_WARN_ENUM_CONVERSION = YES;
204 | CLANG_WARN_INFINITE_RECURSION = YES;
205 | CLANG_WARN_INT_CONVERSION = YES;
206 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
207 | CLANG_WARN_UNREACHABLE_CODE = YES;
208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
209 | COMBINE_HIDPI_IMAGES = YES;
210 | COPY_PHASE_STRIP = YES;
211 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
212 | ENABLE_STRICT_OBJC_MSGSEND = YES;
213 | GCC_C_LANGUAGE_STANDARD = gnu99;
214 | GCC_ENABLE_OBJC_EXCEPTIONS = YES;
215 | GCC_NO_COMMON_BLOCKS = YES;
216 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
218 | GCC_WARN_ABOUT_RETURN_TYPE = YES;
219 | GCC_WARN_UNDECLARED_SELECTOR = YES;
220 | GCC_WARN_UNINITIALIZED_AUTOS = YES;
221 | GCC_WARN_UNUSED_FUNCTION = YES;
222 | GCC_WARN_UNUSED_VARIABLE = YES;
223 | MACOSX_DEPLOYMENT_TARGET = 10.6;
224 | SDKROOT = macosx;
225 | SKIP_INSTALL = YES;
226 | };
227 | name = Release;
228 | };
229 | 734C37B315414CE200994189 /* Debug */ = {
230 | isa = XCBuildConfiguration;
231 | buildSettings = {
232 | CODE_SIGN_IDENTITY = "Mac Developer";
233 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
234 | INFOPLIST_FILE = "LaunchAtLoginHelper/LaunchAtLoginHelper-Info.plist";
235 | MACOSX_DEPLOYMENT_TARGET = 10.6.8;
236 | PRODUCT_BUNDLE_IDENTIFIER = com.producthunt.producthuntosx.LaunchAtLoginHelper;
237 | PRODUCT_NAME = "$(TARGET_NAME)";
238 | PROVISIONING_PROFILE = "";
239 | WRAPPER_EXTENSION = app;
240 | };
241 | name = Debug;
242 | };
243 | 734C37B415414CE200994189 /* Release */ = {
244 | isa = XCBuildConfiguration;
245 | buildSettings = {
246 | CODE_SIGN_IDENTITY = "Mac Developer";
247 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
248 | INFOPLIST_FILE = "LaunchAtLoginHelper/LaunchAtLoginHelper-Info.plist";
249 | MACOSX_DEPLOYMENT_TARGET = 10.6.8;
250 | PRODUCT_BUNDLE_IDENTIFIER = com.producthunt.producthuntosx.LaunchAtLoginHelper;
251 | PRODUCT_NAME = "$(TARGET_NAME)";
252 | PROVISIONING_PROFILE = "";
253 | WRAPPER_EXTENSION = app;
254 | };
255 | name = Release;
256 | };
257 | E7AC4E021CA945340099AB0E /* Testing */ = {
258 | isa = XCBuildConfiguration;
259 | buildSettings = {
260 | ALWAYS_SEARCH_USER_PATHS = NO;
261 | CLANG_WARN_BOOL_CONVERSION = YES;
262 | CLANG_WARN_CONSTANT_CONVERSION = YES;
263 | CLANG_WARN_EMPTY_BODY = YES;
264 | CLANG_WARN_ENUM_CONVERSION = YES;
265 | CLANG_WARN_INFINITE_RECURSION = YES;
266 | CLANG_WARN_INT_CONVERSION = YES;
267 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
268 | CLANG_WARN_UNREACHABLE_CODE = YES;
269 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
270 | COMBINE_HIDPI_IMAGES = YES;
271 | COPY_PHASE_STRIP = NO;
272 | ENABLE_STRICT_OBJC_MSGSEND = YES;
273 | ENABLE_TESTABILITY = YES;
274 | GCC_C_LANGUAGE_STANDARD = gnu99;
275 | GCC_DYNAMIC_NO_PIC = NO;
276 | GCC_ENABLE_OBJC_EXCEPTIONS = YES;
277 | GCC_NO_COMMON_BLOCKS = YES;
278 | GCC_OPTIMIZATION_LEVEL = 0;
279 | GCC_PREPROCESSOR_DEFINITIONS = (
280 | "DEBUG=1",
281 | "$(inherited)",
282 | );
283 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
284 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
285 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
286 | GCC_WARN_ABOUT_RETURN_TYPE = YES;
287 | GCC_WARN_UNDECLARED_SELECTOR = YES;
288 | GCC_WARN_UNINITIALIZED_AUTOS = YES;
289 | GCC_WARN_UNUSED_FUNCTION = YES;
290 | GCC_WARN_UNUSED_VARIABLE = YES;
291 | MACOSX_DEPLOYMENT_TARGET = 10.6;
292 | ONLY_ACTIVE_ARCH = YES;
293 | SDKROOT = macosx;
294 | SKIP_INSTALL = YES;
295 | };
296 | name = Testing;
297 | };
298 | E7AC4E031CA945340099AB0E /* Testing */ = {
299 | isa = XCBuildConfiguration;
300 | buildSettings = {
301 | CODE_SIGN_IDENTITY = "Mac Developer";
302 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
303 | INFOPLIST_FILE = "LaunchAtLoginHelper/LaunchAtLoginHelper-Info.plist";
304 | MACOSX_DEPLOYMENT_TARGET = 10.6.8;
305 | PRODUCT_BUNDLE_IDENTIFIER = com.producthunt.producthuntosx.LaunchAtLoginHelper;
306 | PRODUCT_NAME = "$(TARGET_NAME)";
307 | PROVISIONING_PROFILE = "";
308 | WRAPPER_EXTENSION = app;
309 | };
310 | name = Testing;
311 | };
312 | /* End XCBuildConfiguration section */
313 |
314 | /* Begin XCConfigurationList section */
315 | 734C378E15414CE200994189 /* Build configuration list for PBXProject "LaunchAtLoginHelper" */ = {
316 | isa = XCConfigurationList;
317 | buildConfigurations = (
318 | 734C37B015414CE200994189 /* Debug */,
319 | E7AC4E021CA945340099AB0E /* Testing */,
320 | 734C37B115414CE200994189 /* Release */,
321 | );
322 | defaultConfigurationIsVisible = 0;
323 | defaultConfigurationName = Release;
324 | };
325 | 734C37B215414CE200994189 /* Build configuration list for PBXNativeTarget "LaunchAtLoginHelper" */ = {
326 | isa = XCConfigurationList;
327 | buildConfigurations = (
328 | 734C37B315414CE200994189 /* Debug */,
329 | E7AC4E031CA945340099AB0E /* Testing */,
330 | 734C37B415414CE200994189 /* Release */,
331 | );
332 | defaultConfigurationIsVisible = 0;
333 | defaultConfigurationName = Release;
334 | };
335 | /* End XCConfigurationList section */
336 | };
337 | rootObject = 734C378B15414CE200994189 /* Project object */;
338 | }
339 |
--------------------------------------------------------------------------------