├── .gitignore
├── .travis.yml
├── 3dPartyLicenses.rtf
├── App Reviews.dot
├── App Reviews.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ ├── App Reviews Appstore.xcscheme
│ └── App Reviews.xcscheme
├── App Reviews.xcworkspace
└── contents.xcworkspacedata
├── AppReviews
├── AboutViewController.swift
├── AppDelegate.swift
├── AppReviews.entitlements
├── AppReviews.xcdatamodeld
│ ├── .xccurrentversion
│ └── AppReviews.xcdatamodel
│ │ └── contents
├── ApplicationArrayController.swift
├── ApplicationCellView.swift
├── ApplicationViewController.swift
├── ApplicationWindowController.swift
├── AutoSizedTextField.swift
├── Base.lproj
│ └── Main.storyboard
├── Images.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon_128x128.png
│ │ ├── Icon_128x128@2x.png
│ │ ├── Icon_16x16.png
│ │ ├── Icon_16x16@2x.png
│ │ ├── Icon_256x256.png
│ │ ├── Icon_256x256@2x.png
│ │ ├── Icon_32x32.png
│ │ ├── Icon_32x32@2x.png
│ │ ├── Icon_512x512.png
│ │ └── Icon_512x512@2x.png
│ ├── Contents.json
│ ├── sidebar-icon.imageset
│ │ ├── Contents.json
│ │ └── sidebar-icon.png
│ ├── star-highlighted.imageset
│ │ ├── Contents.json
│ │ └── star-highlighted@2x.png
│ ├── star.imageset
│ │ ├── Contents.json
│ │ └── star@2x.png
│ ├── stausBarIcon.imageset
│ │ ├── Contents.json
│ │ ├── stausBarIcon.png
│ │ └── stausBarIcon@2x.png
│ ├── stausBarIconHappy.imageset
│ │ ├── Contents.json
│ │ ├── stausBarIconHappy.png
│ │ └── stausBarIconHappy@2x.png
│ ├── stausBarIconNeutral.imageset
│ │ ├── Contents.json
│ │ ├── stausBarIconNeutral.png
│ │ └── stausBarIconNeutral@2x.png
│ └── stausBarIconSad.imageset
│ │ ├── Contents.json
│ │ ├── stausBarIconSad.png
│ │ └── stausBarIconSad@2x.png
├── Info.plist
├── LaunchViewController.swift
├── LegalViewController.swift
├── NSApplication+Startup.swift
├── NSApplicationDelegate+Version.swift
├── NSColor+AppReviews.swift
├── NSImageView+Networking.swift
├── NSUserDefaults+AppReviews.swift
├── NotificationsHandler.swift
├── Objective-C-BridgingHeader.h
├── PieChart
│ ├── BackgroundView.h
│ ├── BackgroundView.m
│ ├── NSColor+CGColor.h
│ ├── NSColor+CGColor.m
│ ├── PieChart.h
│ ├── PieChart.m
│ ├── SliceLayer.h
│ └── SliceLayer.m
├── ReviewArrayController.swift
├── ReviewCellView.swift
├── ReviewMenuViewController.swift
├── ReviewPieChartController.swift
├── ReviewSplitViewController.swift
├── ReviewViewController.swift
├── ReviewWindowController.swift
├── SearchViewController.swift
├── StatusMenuController.swift
├── String+Size.swift
└── TableImageCellTransformer.swift
├── AppReviewsTests
├── AppReviewsTests.swift
├── AppVersionTests.swift
├── Info.plist
├── ItunesUrlHandlerTest.swift
├── application.json
└── reviews.json
├── Crashlytics.framework
├── Crashlytics
├── Headers
├── Modules
├── Resources
├── Versions
│ ├── A
│ │ ├── Crashlytics
│ │ ├── Headers
│ │ │ ├── ANSCompatibility.h
│ │ │ ├── Answers.h
│ │ │ ├── CLSAttributes.h
│ │ │ ├── CLSLogging.h
│ │ │ ├── CLSReport.h
│ │ │ ├── CLSStackFrame.h
│ │ │ └── Crashlytics.h
│ │ ├── Modules
│ │ │ └── module.modulemap
│ │ ├── Resources
│ │ │ ├── Info.plist
│ │ │ └── en.lproj
│ │ │ │ └── InfoPlist.strings
│ │ └── _CodeSignature
│ │ │ └── CodeResources
│ └── Current
├── run
├── submit
└── uploadDSYM
├── Fabric.framework
├── Fabric
├── Headers
├── Modules
├── Resources
├── Versions
│ ├── A
│ │ ├── Fabric
│ │ ├── Headers
│ │ │ ├── FABAttributes.h
│ │ │ └── Fabric.h
│ │ ├── Modules
│ │ │ └── module.modulemap
│ │ └── Resources
│ │ │ └── Info.plist
│ └── Current
├── run
└── uploadDSYM
├── LICENSE
├── Playground.playground
├── Contents.swift
├── Sources
│ └── SupportCode.swift
├── contents.xcplayground
└── timeline.xctimeline
├── Podfile
├── Podfile.lock
├── README.md
├── ReviewManager
├── ApplicationUpdater.swift
├── Categories
│ ├── Application+String.swift
│ ├── Array+Utils.swift
│ ├── Review+String.swift
│ ├── String+AppVersion.swift
│ └── String+Emoji.swift
├── DatabaseHandler.swift
├── ItunesService.swift
├── ItunesUrlHandler.swift
├── Models
│ ├── AppReviews.xcdatamodeld
│ │ ├── .xccurrentversion
│ │ └── AppReviews.xcdatamodel
│ │ │ └── contents
│ ├── Application.swift
│ ├── ApplicationSettings.swift
│ └── Review.swift
├── PersistentStack.swift
├── ReviewManager.swift
└── Timer.swift
├── Screenshots
├── add-application-screen.png
├── appreviews-icon-100.jpg
├── appreviews-icon-512.png
├── review-screen.jpg
└── review-screen.png
└── travis
├── before_script.sh
└── script.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 |
22 | # Bundler
23 | .bundle
24 |
25 | # We recommend against adding the Pods directory to your .gitignore. However
26 | # you should judge for yourself, the pros and cons are mentioned at:
27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
28 | #
29 | # Note: if you ignore the Pods directory, make sure to uncomment
30 | # `pod install` in .travis.yml
31 | #
32 | Pods/
33 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | before_script: travis/before_script.sh
3 | script: travis/script.sh
--------------------------------------------------------------------------------
/App Reviews.dot:
--------------------------------------------------------------------------------
1 | digraph G {
2 | node [shape=box];
3 | "SliceLayer" -> {};
4 | "SUAppcastItem" -> "SUExport";
5 | "Sparkle" -> {};
6 | "Pods-SimpleCocoaAnalytics-umbrella" -> "AnalyticsEvent";
7 | "Pods-SimpleCocoaAnalytics-umbrella" -> "AnalyticsHelper";
8 | "EDStarRating" -> {};
9 | "SUExport" -> {};
10 | "Pods-SwiftyJSON-dummy" -> {};
11 | "SUVersionComparisonProtocol" -> "SUExport";
12 | "Objective-C-BridgingHeader" -> "PieChart";
13 | "Pods-SimpleCocoaAnalytics-dummy" -> {};
14 | "Pods-dummy" -> {};
15 | "SUVersionDisplayProtocol" -> "SUExport";
16 | "AnalyticsEvent" -> {};
17 | "SUErrors" -> "SUExport";
18 | "Pods-Alamofire-umbrella" -> {};
19 | "Pods-umbrella" -> {};
20 | "SUAppcast" -> "SUExport";
21 | "Pods-environment" -> {};
22 | "Pods-SwiftyJSON-umbrella" -> {};
23 | "Pods-Alamofire-dummy" -> {};
24 | "BackgroundView" -> {};
25 | "SUUpdater" -> "SUVersionComparisonProtocol";
26 | "SUUpdater" -> "SUVersionDisplayProtocol";
27 | "SUUpdater" -> "SUExport";
28 | "SUStandardVersionComparator" -> "SUVersionComparisonProtocol";
29 | "SUStandardVersionComparator" -> "SUExport";
30 | "Pods-EDStarRating-dummy" -> {};
31 | "PieChart" -> "BackgroundView";
32 | "PieChart" -> "SliceLayer";
33 | "AnalyticsHelper" -> "AnalyticsEvent";
34 | "Pods-EDStarRating-umbrella" -> "EDStarRating";
35 |
36 | "Pods-SwiftyJSON-prefix" [color=red];
37 | "Pods-SwiftyJSON-prefix" -> "Pods-environment" [color=red];
38 | "Pods-Alamofire-prefix" [color=red];
39 | "Pods-Alamofire-prefix" -> "Pods-environment" [color=red];
40 | "Pods-EDStarRating-prefix" [color=red];
41 | "Pods-EDStarRating-prefix" -> "Pods-environment" [color=red];
42 | "Pods-SimpleCocoaAnalytics-prefix" [color=red];
43 | "Pods-SimpleCocoaAnalytics-prefix" -> "Pods-environment" [color=red];
44 |
45 | edge [color=blue, dir=both];
46 |
47 | edge [color=black];
48 | node [shape=plaintext];
49 | "Categories" [label="NSColor+CGColor"];
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/App Reviews.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/App Reviews.xcodeproj/xcshareddata/xcschemes/App Reviews Appstore.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
78 |
80 |
86 |
87 |
88 |
89 |
93 |
94 |
95 |
96 |
97 |
98 |
104 |
106 |
112 |
113 |
114 |
115 |
117 |
118 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/App Reviews.xcodeproj/xcshareddata/xcschemes/App Reviews.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
61 |
67 |
68 |
69 |
70 |
71 |
77 |
78 |
79 |
80 |
81 |
82 |
92 |
94 |
100 |
101 |
102 |
103 |
106 |
107 |
110 |
111 |
112 |
113 |
114 |
115 |
121 |
123 |
129 |
130 |
131 |
132 |
134 |
135 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/App Reviews.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/AppReviews/AboutViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AboutViewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-03.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 | import Cocoa
9 | import Sparkle
10 |
11 | class AboutViewController: NSViewController {
12 |
13 | @IBOutlet weak var versionLabel: NSTextField?
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 | versionLabel?.stringValue = NSApplication.v_versionBuild()
18 | }
19 |
20 | @IBAction func checkForUpdatesClicked(objects:AnyObject?) {
21 | SUUpdater.sharedUpdater().checkForUpdates(objects)
22 | }
23 |
24 | @IBAction func openGitHubClicked(objects: AnyObject?) {
25 | NSWorkspace.sharedWorkspace().openURL(NSURL(string: "https://github.com/knutigro/AppReviews")!)
26 | }
27 |
28 | @IBAction func openProjectPagesClicked(objects: AnyObject?) {
29 | NSWorkspace.sharedWorkspace().openURL(NSURL(string: "http://knutigro.github.io/apps/app-reviews/")!)
30 | }
31 |
32 | @IBAction func openFeedbackClicked(sender: AnyObject) {
33 | NSWorkspace.sharedWorkspace().openURL(NSURL(string: "http://knutigro.github.io/apps/app-reviews/#Feedback")!)
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/AppReviews/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-08.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import AppKit
11 | import SimpleCocoaAnalytics
12 | import Fabric
13 | import Crashlytics
14 | import Sparkle
15 |
16 | @NSApplicationMain
17 | class AppDelegate: NSObject, NSApplicationDelegate {
18 |
19 | lazy var applicationWindowController: NSWindowController = self.initialApplicationWindowController()
20 | lazy var aboutWindowController: NSWindowController = self.initialAboutWindowController()
21 | lazy var reviewsWindowController: ReviewWindowController = self.initialReviewWindowController()
22 | lazy var launchWindowController: NSWindowController = self.initialLaunchWindowController()
23 |
24 | var statusMenuController: StatusMenuController!
25 |
26 | // MARK: Application Process
27 |
28 | func applicationDidFinishLaunching(aNotification: NSNotification) {
29 |
30 | // Fabric CrashAlytics
31 | Fabric.with([Crashlytics()])
32 | NSUserDefaults.standardUserDefaults().registerDefaults(["NSApplicationCrashOnExceptions": true])
33 |
34 | // Google Analytics
35 | let analyticsHelper = AnalyticsHelper.sharedInstance()
36 | analyticsHelper.recordScreenWithName("Launch")
37 | analyticsHelper.beginPeriodicReportingWithAccount("UA-62792522-3", name: "App Reviews OSX", version: NSApplication.v_versionBuild())
38 |
39 | // Create ReviewManager shared object
40 | _ = ReviewManager.start()
41 |
42 | if NSUserDefaults.review_isFirstLaunch() {
43 | NSUserDefaults.review_setDidLaunch()
44 |
45 | // As default set launch at startup
46 | NSApplication.setShouldLaunchAtStartup(true)
47 | }
48 |
49 | // Create StatusMenu
50 | statusMenuController = StatusMenuController()
51 |
52 | // Show Launchscreen
53 | if NSUserDefaults.review_shouldShowLaunchScreen() {
54 | self.launchWindowController.showWindow(self)
55 | NSApp.activateIgnoringOtherApps(true)
56 | }
57 |
58 | // Initialize Sparkle and do a first check
59 | // Since App Reviews is meant to run in background I want
60 | // to force the updater to start allready at first App Launch
61 | SUUpdater.sharedUpdater().checkForUpdatesInBackground()
62 | }
63 |
64 | func applicationWillTerminate(aNotification: NSNotification) {
65 | // Insert code here to tear down your application
66 | AnalyticsHelper.sharedInstance().handleApplicationWillClose()
67 | NSUserDefaults.standardUserDefaults().synchronize()
68 | }
69 |
70 | func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply {
71 | // Saves changes in the application's managed object context before the application terminates.
72 | ReviewManager.saveContext()
73 |
74 | return .TerminateNow
75 | }
76 | }
77 |
78 | // MARK: WindowControllers
79 |
80 | extension AppDelegate {
81 |
82 | func initialLaunchWindowController() -> NSWindowController {
83 | let storyboard = NSStoryboard(name: "Main", bundle: nil)
84 | let windowController = storyboard.instantiateControllerWithIdentifier("LaunchWindowController") as! NSWindowController
85 |
86 | return windowController
87 | }
88 |
89 | func initialApplicationWindowController() -> NSWindowController {
90 | let storyboard = NSStoryboard(name: "Main", bundle: nil)
91 | let windowController = storyboard.instantiateControllerWithIdentifier("ApplicationWindowController") as! NSWindowController
92 |
93 | return windowController
94 | }
95 |
96 | func initialReviewWindowController() -> ReviewWindowController {
97 | let storyboard = NSStoryboard(name: "Main", bundle: nil)
98 | let windowController = storyboard.instantiateControllerWithIdentifier("ReviewWindowsController") as! ReviewWindowController
99 |
100 | return windowController
101 | }
102 |
103 | func initialAboutWindowController() -> NSWindowController {
104 | let storyboard = NSStoryboard(name: "Main", bundle: nil)
105 | let windowController = storyboard.instantiateControllerWithIdentifier("AboutWindowController") as! NSWindowController
106 |
107 | return windowController
108 | }
109 | }
110 |
111 |
--------------------------------------------------------------------------------
/AppReviews/AppReviews.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/AppReviews/AppReviews.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/AppReviews/AppReviews.xcdatamodeld/AppReviews.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/AppReviews/ApplicationArrayController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationArrayController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-12.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class ApplicationArrayController: NSArrayController {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/AppReviews/ApplicationCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationCellView.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-13.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | let kApplicationCellIdentifier = "applicationCell"
12 |
13 | class ApplicationCellView: NSTableCellView {
14 |
15 | @IBOutlet weak var authorTextField: NSTextField?
16 |
17 | override func awakeFromNib() {
18 | super.awakeFromNib()
19 | }
20 |
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/AppReviews/ApplicationViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationViewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-14.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 |
10 | import Cocoa
11 | import SwiftyJSON
12 |
13 | class ApplicationViewController: NSViewController {
14 |
15 | @IBOutlet weak var tableView: NSTableView!
16 | @IBOutlet var applicationArrayController: ApplicationArrayController?
17 | var applications = [Application]()
18 |
19 | var managedObjectContext: NSManagedObjectContext!
20 |
21 | // MARK: - Init & Teardown
22 |
23 | required init?(coder: NSCoder) {
24 | super.init(coder: coder)
25 | managedObjectContext = ReviewManager.managedObjectContext()
26 | }
27 |
28 | // MARK: - View & Navigation
29 |
30 | override func viewDidLoad() {
31 | super.viewDidLoad()
32 | }
33 | }
34 |
35 | // Mark: - Actions
36 |
37 | extension ApplicationViewController {
38 |
39 | @IBAction func removeButtonClicked(objects:AnyObject?) {
40 | if let applications = objects as? [Application], let rowNumber = tableView?.selectedRow {
41 | if applications.count > rowNumber && rowNumber >= 0{
42 | DatabaseHandler.removeApplication(applications[rowNumber].objectID)
43 | }
44 | }
45 | }
46 |
47 | func cellDoubleClicked(applications: [Application]?) {
48 | if let applications = applications, let rowNumber = tableView?.selectedRow {
49 | if applications.count > rowNumber && rowNumber >= 0{
50 | let application = applications[rowNumber]
51 | ReviewWindowController.show(application.objectID)
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/AppReviews/ApplicationWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationWindow.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-21.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 | import SwiftyJSON
11 |
12 | class ApplicationWindowController: NSWindowController {
13 | @IBOutlet weak var searchField: NSSearchField?
14 | var searchWindowController: NSWindowController?
15 | }
16 |
17 | // MARK: - SearchField
18 |
19 | extension ApplicationWindowController {
20 |
21 | override func controlTextDidEndEditing(notification: NSNotification) {
22 | if let textField = notification.object as? NSTextField {
23 | if !textField.stringValue.isEmpty {
24 | openSearchResultController()
25 | searchApp(textField.stringValue)
26 | }
27 | }
28 | }
29 | }
30 |
31 | // MARK: - Search Apps
32 |
33 | extension ApplicationWindowController {
34 |
35 | func searchApp(name: String) {
36 |
37 | ItunesService.fetchApplications(name) { [weak self]
38 | (success: Bool, applications: JSON?, error: NSError?)
39 | in
40 |
41 | let _ = success as Bool
42 | let blockError = error
43 |
44 | if blockError != nil {
45 | print("error: " + blockError!.localizedDescription)
46 | }
47 |
48 | if let applications = applications?.arrayValue {
49 | self?.openSearchResult(applications)
50 | }
51 | }
52 | }
53 |
54 | func openSearchResult(items: [JSON]) {
55 |
56 | if searchWindowController == nil {
57 | openSearchResultController()
58 | }
59 | if let searchViewController = searchWindowController?.window?.contentViewController as? SearchViewController {
60 | searchViewController.items = items
61 | searchViewController.tableView?.reloadData()
62 | searchViewController.state = .Idle
63 | }
64 | }
65 |
66 | func openSearchResultController() {
67 | let storyboard = NSStoryboard(name: "Main", bundle: nil)
68 | searchWindowController = storyboard.instantiateControllerWithIdentifier("SearchResultWindowController") as? NSWindowController
69 | let window = searchWindowController?.window
70 |
71 | if let searchViewController = window?.contentViewController as? SearchViewController {
72 | searchViewController.delegate = self
73 | searchViewController.state = .Loading
74 | }
75 |
76 | self.window?.beginSheet(window!) {
77 | (returnCode: NSModalResponse)
78 | in
79 | self.searchWindowController = nil
80 | }
81 | }
82 | }
83 |
84 | // Mark: - SearchViewControllerDelegate
85 |
86 | extension ApplicationWindowController: SearchViewControllerDelegate {
87 | func searchViewController(searchViewController: SearchViewController, didSelectApplication application: JSON) {
88 | searchField?.stringValue = ""
89 |
90 | DatabaseHandler.saveApplication(application)
91 |
92 | if let searchWindow = searchWindowController?.window {
93 | window?.endSheet(searchWindow)
94 | }
95 | }
96 |
97 | func searchViewControllerDidCancel(searchViewController: SearchViewController) {
98 | searchField?.stringValue = ""
99 | if let searchWindow = searchWindowController?.window {
100 | window?.endSheet(searchWindow)
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/AppReviews/AutoSizedTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSTextField+Size.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-20.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class AutoSizedTextField: NSTextField {
12 |
13 | override var intrinsicContentSize: NSSize {
14 | if cell?.wraps == nil{
15 | return super.intrinsicContentSize
16 | }
17 |
18 | var frame = self.frame
19 | let width = frame.size.width
20 |
21 | // Make the frame very high, while keeping the width
22 | frame.size.height = CGFloat.max
23 |
24 | // Calculate new height within the frame
25 | // with practically infinite height.
26 |
27 | let height = cell?.cellSizeForBounds(frame).height
28 |
29 | return NSMakeSize(width, height!)
30 | }
31 |
32 | required init?(coder aDecoder: NSCoder) {
33 | super.init(coder: aDecoder)
34 | translatesAutoresizingMaskIntoConstraints = false
35 | invalidateIntrinsicContentSize()
36 | }
37 |
38 | override func textDidChange(notification: NSNotification) {
39 | super.textDidChange(notification)
40 | invalidateIntrinsicContentSize()
41 | }
42 | }
43 |
44 |
45 | extension NSTextFieldCell {
46 |
47 | func scaleToAspectFit(source: CGSize, into: CGSize, padding: CGFloat) -> CGFloat {
48 | let width = (into.width - padding) / source.width
49 | let height = (into.height - padding) / source.height
50 |
51 | return min(width, height)
52 | }
53 |
54 | func scaleToAspectFit(size:CGSize, text: String, font: NSFont) {
55 | let sampleFont = NSFont(descriptor: font.fontDescriptor, size: 12)!
56 | let sampleSize = (text as NSString).sizeWithAttributes([NSFontAttributeName: sampleFont])
57 | let _ = scaleToAspectFit(sampleSize, into: size, padding: 10)
58 |
59 | }
60 |
61 |
62 | }
--------------------------------------------------------------------------------
/AppReviews/Images.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 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_128x128.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_128x128@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_16x16.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_16x16@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_256x256.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_256x256@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_32x32.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_32x32@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_512x512.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/AppIcon.appiconset/Icon_512x512@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/sidebar-icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "sidebar-icon.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/sidebar-icon.imageset/sidebar-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/sidebar-icon.imageset/sidebar-icon.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/star-highlighted.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "star-highlighted@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/star-highlighted.imageset/star-highlighted@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/star-highlighted.imageset/star-highlighted@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/star.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "star@2x.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/star.imageset/star@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/star.imageset/star@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "stausBarIcon.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "stausBarIcon@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIcon.imageset/stausBarIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIcon.imageset/stausBarIcon.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIcon.imageset/stausBarIcon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIcon.imageset/stausBarIcon@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconHappy.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "stausBarIconHappy.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "stausBarIconHappy@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconHappy.imageset/stausBarIconHappy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIconHappy.imageset/stausBarIconHappy.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconHappy.imageset/stausBarIconHappy@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIconHappy.imageset/stausBarIconHappy@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconNeutral.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "stausBarIconNeutral.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "stausBarIconNeutral@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconNeutral.imageset/stausBarIconNeutral.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIconNeutral.imageset/stausBarIconNeutral.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconNeutral.imageset/stausBarIconNeutral@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIconNeutral.imageset/stausBarIconNeutral@2x.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconSad.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "stausBarIconSad.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "stausBarIconSad@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconSad.imageset/stausBarIconSad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIconSad.imageset/stausBarIconSad.png
--------------------------------------------------------------------------------
/AppReviews/Images.xcassets/stausBarIconSad.imageset/stausBarIconSad@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/AppReviews/Images.xcassets/stausBarIconSad.imageset/stausBarIconSad@2x.png
--------------------------------------------------------------------------------
/AppReviews/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSAllowsArbitraryLoads
8 |
9 |
10 | CFBundleDevelopmentRegion
11 | en
12 | CFBundleExecutable
13 | $(EXECUTABLE_NAME)
14 | CFBundleIconFile
15 |
16 | CFBundleIdentifier
17 | com.cocmoc.appreviews
18 | CFBundleInfoDictionaryVersion
19 | 6.0
20 | CFBundleName
21 | $(PRODUCT_NAME)
22 | CFBundlePackageType
23 | APPL
24 | CFBundleShortVersionString
25 | 0.0.32
26 | CFBundleSignature
27 | ????
28 | CFBundleVersion
29 | 10
30 | Fabric
31 |
32 | APIKey
33 | 975b8be9fe2f4dfd2f635a7b54ac52b4c49e023b
34 | Kits
35 |
36 |
37 | KitInfo
38 |
39 | KitName
40 | Crashlytics
41 |
42 |
43 |
44 | LSApplicationCategoryType
45 | public.app-category.developer-tools
46 | LSMinimumSystemVersion
47 | $(MACOSX_DEPLOYMENT_TARGET)
48 | LSUIElement
49 |
50 | NSHumanReadableCopyright
51 | Copyright © 2015 Cocmoc. All rights reserved.
52 | NSMainStoryboardFile
53 | Main
54 | NSPrincipalClass
55 | NSApplication
56 | SUFeedURL
57 | http://knutigro.github.io/apps/app-reviews/app-reviews-appcast.xml
58 |
59 |
60 |
--------------------------------------------------------------------------------
/AppReviews/LaunchViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LaunchScreenViewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-28.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class LaunchViewController: NSViewController {
12 |
13 | @IBOutlet weak var versionLabel: NSTextField?
14 | @IBOutlet weak var checkButton: NSButton?
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | versionLabel?.stringValue = NSApplication.v_versionBuild()
19 | }
20 |
21 | @IBAction func showButtonDidCheck(checkButton: NSButton) {
22 | NSUserDefaults.review_setShouldShowLaunchScreen(checkButton.state == NSOffState ? false : true)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/AppReviews/LegalViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LegalViewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-27.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class LegalViewController: NSViewController {
12 |
13 | var textView: NSTextView? {
14 | let scrollView = view as? NSScrollView
15 | return scrollView?.contentView.documentView as? NSTextView
16 | }
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | let rtfFilePath = NSBundle.mainBundle().pathForResource("3dPartyLicenses", ofType: "rtf")
21 | textView?.readRTFDFromFile(rtfFilePath!)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AppReviews/NSApplication+Startup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSApplication+Startup.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-29.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | extension NSApplication {
10 |
11 | // MARK: Public methods
12 |
13 | class func shouldLaunchAtStartup() -> Bool {
14 | return (itemReferencesInLoginItems().existingReference != nil)
15 | }
16 |
17 | class func toggleShouldLaunchAtStartup() {
18 | let itemReferences = itemReferencesInLoginItems()
19 | if (itemReferences.existingReference == nil) {
20 | NSApplication.addToStartupItems(itemReferences.lastReference)
21 | } else {
22 | NSApplication.removeFromStartupItems(itemReferences.existingReference)
23 | }
24 | }
25 |
26 | class func setShouldLaunchAtStartup(launchAtStartup : Bool) {
27 | let itemReferences = itemReferencesInLoginItems()
28 | if (launchAtStartup && itemReferences.existingReference == nil) {
29 | NSApplication.addToStartupItems(itemReferences.lastReference)
30 | } else if (!launchAtStartup) {
31 | NSApplication.removeFromStartupItems(itemReferences.existingReference)
32 | }
33 | }
34 |
35 | // MARK: Private methods
36 |
37 | private class func itemReferencesInLoginItems() -> (existingReference: LSSharedFileListItemRef?, lastReference: LSSharedFileListItemRef?) {
38 |
39 | let itemUrl : UnsafeMutablePointer?> = UnsafeMutablePointer?>.alloc(1)
40 | if let appUrl : NSURL = NSURL.fileURLWithPath(NSBundle.mainBundle().bundlePath) {
41 | let loginItemsRef = LSSharedFileListCreate(
42 | nil,
43 | kLSSharedFileListSessionLoginItems.takeRetainedValue(),
44 | nil
45 | ).takeRetainedValue() as LSSharedFileListRef?
46 | if loginItemsRef != nil {
47 | let loginItems: NSArray = LSSharedFileListCopySnapshot(loginItemsRef, nil).takeRetainedValue() as NSArray
48 | let lastItemRef: LSSharedFileListItemRef? = (loginItems.lastObject as! LSSharedFileListItemRef)
49 | for var i = 0; i < loginItems.count; ++i {
50 |
51 | let currentItemRef: LSSharedFileListItemRef = loginItems.objectAtIndex(i) as! LSSharedFileListItemRef
52 | // let url = LSSharedFileListItemCopyResolvedURL(currentItemRef, 0, nil).takeRetainedValue()
53 |
54 | if LSSharedFileListItemResolve(currentItemRef, 0, itemUrl, nil) == noErr {
55 | if let urlRef: NSURL = itemUrl.memory?.takeRetainedValue() {
56 | if urlRef.isEqual(appUrl) {
57 | return (currentItemRef, lastItemRef)
58 | }
59 | }
60 | } else {
61 | print("Unknown login application")
62 | }
63 | }
64 | //The application was not found in the startup list
65 | return (nil, lastItemRef)
66 | }
67 | }
68 |
69 | return (nil, nil)
70 | }
71 |
72 | private class func removeFromStartupItems(existingReference: LSSharedFileListItemRef?) {
73 | if let existingReference = existingReference,
74 | let loginItemsRef = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() as LSSharedFileListRef? {
75 | LSSharedFileListItemRemove(loginItemsRef, existingReference);
76 | }
77 | }
78 |
79 | private class func addToStartupItems(lastReference: LSSharedFileListItemRef?) {
80 | if let lastReference = lastReference,
81 | let loginItemsRef = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() as LSSharedFileListRef? {
82 | if let appUrl : CFURLRef = NSURL.fileURLWithPath(NSBundle.mainBundle().bundlePath) {
83 | LSSharedFileListInsertItemURL(loginItemsRef, lastReference, nil, nil, appUrl, nil, nil)
84 | }
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/AppReviews/NSApplicationDelegate+Version.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSApplicationDelegate+Version.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-03.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 |
10 | import AppKit
11 |
12 | // MARK: Version
13 |
14 | extension NSApplication {
15 |
16 | class func v_appVersion() -> String? {
17 | return NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleShortVersionString") as? String
18 | }
19 |
20 | class func v_build() -> String? {
21 | return NSBundle.mainBundle().objectForInfoDictionaryKey(kCFBundleVersionKey as String) as? String
22 | }
23 |
24 | class func v_versionBuild() -> String {
25 | var versionBuild = ""
26 |
27 | if let version = NSApplication.v_appVersion() {
28 | versionBuild += String(format: "Version %@", version)
29 | }
30 |
31 | if let buildString = NSApplication.v_build() {
32 | versionBuild += String(format: " (%@)", buildString)
33 | }
34 |
35 | return versionBuild
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/AppReviews/NSColor+AppReviews.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSColor+AppReviews.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-09.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 |
10 | extension NSColor {
11 |
12 | class func reviewRed() -> NSColor {
13 | return NSColor(deviceRed: 0.941, green: 0.306, blue: 0.314, alpha: 1)
14 | }
15 |
16 | class func reviewOrange() -> NSColor {
17 | return NSColor(deviceRed: 0.992, green: 0.522, blue: 0.224, alpha: 1)
18 | }
19 |
20 | class func reviewYellow() -> NSColor {
21 | return NSColor(deviceRed: 0.992, green: 0.741, blue: 0.239, alpha: 1)
22 | }
23 |
24 | class func reviewBlue() -> NSColor {
25 | return NSColor(deviceRed: 0.404, green: 0.608, blue: 0.788, alpha: 1)
26 | }
27 |
28 | class func reviewGreen() -> NSColor {
29 | return NSColor(deviceRed: 0.353, green: 0.788, blue: 0.765, alpha: 1)
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/AppReviews/NSImageView+Networking.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSImageView+Networking.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-13.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | protocol AFImageCacheProtocol:class{
12 | func cachedImageForRequest(request:NSURLRequest) -> NSImage?
13 | func cacheImage(image:NSImage, forRequest request:NSURLRequest);
14 | }
15 |
16 | extension NSImageView {
17 | private struct AssociatedKeys {
18 | static var SharedImageCache = "SharedImageCache"
19 | static var RequestImageOperation = "RequestImageOperation"
20 | static var URLRequestImage = "UrlRequestImage"
21 | }
22 |
23 | class func setSharedImageCache(cache:AFImageCacheProtocol?) {
24 | objc_setAssociatedObject(self, &AssociatedKeys.SharedImageCache, cache, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY)
25 | }
26 |
27 | class func sharedImageCache() -> AFImageCacheProtocol {
28 | struct Static {
29 | static var token: dispatch_once_t = 0
30 | static var defaultImageCache:AFImageCache?
31 | }
32 | dispatch_once(&Static.token, { () -> Void in
33 | Static.defaultImageCache = AFImageCache()
34 | })
35 | return objc_getAssociatedObject(self, &AssociatedKeys.SharedImageCache) as? AFImageCache ?? Static.defaultImageCache!
36 | }
37 |
38 | class func af_sharedImageRequestOperationQueue() -> NSOperationQueue {
39 | struct Static {
40 | static var token:dispatch_once_t = 0
41 | static var queue:NSOperationQueue?
42 | }
43 |
44 | dispatch_once(&Static.token, { () -> Void in
45 | Static.queue = NSOperationQueue()
46 | Static.queue!.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount
47 | })
48 | return Static.queue!
49 | }
50 |
51 | private var af_requestImageOperation:(operation:NSOperation?, request: NSURLRequest?) {
52 | get {
53 | let operation:NSOperation? = objc_getAssociatedObject(self, &AssociatedKeys.RequestImageOperation) as? NSOperation
54 | let request:NSURLRequest? = objc_getAssociatedObject(self, &AssociatedKeys.URLRequestImage) as? NSURLRequest
55 | return (operation, request)
56 | }
57 | set {
58 | objc_setAssociatedObject(self, &AssociatedKeys.RequestImageOperation, newValue.operation, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
59 | objc_setAssociatedObject(self, &AssociatedKeys.URLRequestImage, newValue.request, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
60 | }
61 | }
62 |
63 | func setImageWithUrl(url:NSURL, placeHolderImage:NSImage? = nil) {
64 | let request:NSMutableURLRequest = NSMutableURLRequest(URL: url)
65 | request.addValue("image/*", forHTTPHeaderField: "Accept")
66 | setImageWithUrlRequest(request, placeHolderImage: placeHolderImage, success: nil, failure: nil)
67 | }
68 |
69 | func setImageWithUrlRequest(request:NSURLRequest, placeHolderImage:NSImage? = nil,
70 | success:((request:NSURLRequest?, response:NSURLResponse?, image:NSImage) -> Void)?,
71 | failure:((request:NSURLRequest?, response:NSURLResponse?, error:NSError) -> Void)?)
72 | {
73 | cancelImageRequestOperation()
74 |
75 | if let cachedImage = NSImageView.sharedImageCache().cachedImageForRequest(request) {
76 | if success != nil {
77 | success!(request: nil, response:nil, image: cachedImage)
78 | }
79 | else {
80 | image = cachedImage
81 | }
82 |
83 | return
84 | }
85 |
86 | if placeHolderImage != nil {
87 | image = placeHolderImage
88 | }
89 |
90 | af_requestImageOperation = (NSBlockOperation(block: { () -> Void in
91 | var response:NSURLResponse?
92 | var error:NSError?
93 | let data: NSData?
94 | do {
95 | data = try NSURLConnection.sendSynchronousRequest(request, returningResponse: &response)
96 | } catch let error1 as NSError {
97 | error = error1
98 | data = nil
99 | } catch {
100 | fatalError()
101 | }
102 | dispatch_async(dispatch_get_main_queue(), { () -> Void in
103 | if request.URL!.isEqual(self.af_requestImageOperation.request?.URL) {
104 | let image:NSImage? = (data != nil ? NSImage(data: data!): nil)
105 | if image != nil {
106 | if success != nil {
107 | success!(request: request, response: response, image: image!)
108 | }
109 | else {
110 | self.image = image!
111 | }
112 | }
113 | else {
114 | if failure != nil {
115 | failure!(request: request, response:response, error: error!)
116 | }
117 | }
118 |
119 | self.af_requestImageOperation = (nil, nil)
120 | }
121 | })
122 | }), request)
123 |
124 | NSImageView.af_sharedImageRequestOperationQueue().addOperation(af_requestImageOperation.operation!)
125 | }
126 |
127 | private func cancelImageRequestOperation() {
128 | af_requestImageOperation.operation?.cancel()
129 | af_requestImageOperation = (nil, nil)
130 | }
131 | }
132 |
133 | func AFImageCacheKeyFromURLRequest(request:NSURLRequest) -> String {
134 | return request.URL!.absoluteString
135 | }
136 |
137 | class AFImageCache: NSCache, AFImageCacheProtocol {
138 | func cachedImageForRequest(request: NSURLRequest) -> NSImage? {
139 | switch request.cachePolicy {
140 | case NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData,
141 | NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData:
142 | return nil
143 | default:
144 | break
145 | }
146 |
147 | return objectForKey(AFImageCacheKeyFromURLRequest(request)) as? NSImage
148 | }
149 |
150 | func cacheImage(image: NSImage, forRequest request: NSURLRequest) {
151 | setObject(image, forKey: AFImageCacheKeyFromURLRequest(request))
152 | }
153 | }
--------------------------------------------------------------------------------
/AppReviews/NSUserDefaults+AppReviews.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSUserDefaults+AppReviews.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-28.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // AppReviews extension
12 |
13 | extension NSUserDefaults {
14 |
15 | class func review_shouldShowLaunchScreen() -> Bool {
16 | return !NSUserDefaults.standardUserDefaults().boolForKey("ShouldNotShowLaunchScreen")
17 | }
18 |
19 | class func review_setShouldShowLaunchScreen(show : Bool) {
20 | NSUserDefaults.standardUserDefaults().setBool(!show, forKey: "ShouldNotShowLaunchScreen")
21 | NSUserDefaults.standardUserDefaults().synchronize()
22 | }
23 |
24 | class func review_isFirstLaunch() -> Bool {
25 | return !NSUserDefaults.standardUserDefaults().boolForKey("DidRun")
26 | }
27 |
28 | class func review_setDidLaunch() {
29 | NSUserDefaults.standardUserDefaults().setBool(true, forKey: "DidRun")
30 | NSUserDefaults.standardUserDefaults().synchronize()
31 | }
32 |
33 | class func review_isLeftMenuCollapsed() -> Bool {
34 | return NSUserDefaults.standardUserDefaults().boolForKey("LeftMenuCollapsed")
35 | }
36 |
37 | class func review_setLeftMenuCollapsed(collapsed: Bool) {
38 | NSUserDefaults.standardUserDefaults().setBool(collapsed, forKey: "LeftMenuCollapsed")
39 | NSUserDefaults.standardUserDefaults().synchronize()
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/AppReviews/NotificationsHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationsHandler.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-02.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AppKit
11 |
12 | let kNotificaObjectIdKey = "kNotificaObjectIdKey"
13 |
14 | @objc
15 | class NotificationsHandler: NSObject {
16 |
17 | // MARK: - Init & teardown
18 |
19 | override init() {
20 | super.init()
21 |
22 | _ = NSNotificationCenter.defaultCenter().addObserverForName(kDidAddReviewsNotification, object: nil, queue: nil) { [weak self] notification in
23 |
24 | guard let newReviewsIds = notification.object as? Set else { return }
25 |
26 | var newReviews = [Application: [Review]]()
27 | for objectID in newReviewsIds {
28 | if let newReview = Review.getWithId(objectID, context: ReviewManager.managedObjectContext()) {
29 | if newReviews[newReview.application]?.append(newReview) == nil {
30 | var reviewArray = [Review]()
31 | reviewArray.append(newReview)
32 | newReviews[newReview.application] = reviewArray
33 | }
34 | }
35 | }
36 |
37 | for application in newReviews.keys {
38 | if let reviews = newReviews[application] {
39 | self?.newReviewsNotification(application, reviews: reviews)
40 | }
41 | }
42 | }
43 | }
44 |
45 | func newReviewsNotification(application: Application, reviews: [Review]) {
46 | assert(reviews.count > 0, "Reviews should be greater than 0")
47 | if reviews.count == 0 { return }
48 |
49 | var stars = ""
50 | if reviews.count > 1 {
51 | var totalRating = 0
52 | for review in reviews {
53 | totalRating += review.rating.integerValue
54 | }
55 | stars = Int(totalRating / reviews.count).toEmojiStars()
56 | } else {
57 | stars = reviews[0].rating.integerValue.toEmojiStars()
58 | }
59 |
60 | var message = (NSString(format: NSLocalizedString("%@ new review%@. %@", comment: "review.notification.reviewstext"), String(reviews.count), (reviews.count > 1 ? "s": ""), stars)) as String
61 |
62 | if (!reviews[0].title.isEmpty) {
63 | message = message + "\n" + reviews[0].title
64 | }
65 |
66 | let notification:NSUserNotification = NSUserNotification()
67 | notification.title = application.trackName
68 |
69 | let urlString = application.objectID.URIRepresentation().absoluteString
70 | notification.userInfo = [kNotificaObjectIdKey: urlString]
71 |
72 | notification.informativeText = message
73 | notification.actionButtonTitle = NSLocalizedString("Open Reviews", comment: "review.notification.openReviews")
74 | notification.hasActionButton = true
75 | let center: NSUserNotificationCenter = NSUserNotificationCenter.defaultUserNotificationCenter()
76 | center.delegate = self
77 |
78 | dispatch_async(dispatch_get_main_queue(), { () -> Void in
79 | center.scheduleNotification(notification)
80 | })
81 | }
82 | }
83 |
84 | extension NotificationsHandler: NSUserNotificationCenterDelegate {
85 |
86 | func userNotificationCenter(center: NSUserNotificationCenter, didDeliverNotification notification: NSUserNotification) {
87 | }
88 |
89 | func userNotificationCenter(center: NSUserNotificationCenter, shouldPresentNotification notification: NSUserNotification) -> Bool {
90 | return true
91 | }
92 |
93 | func userNotificationCenter(center: NSUserNotificationCenter, didActivateNotification notification: NSUserNotification) {
94 | guard let urlString = notification.userInfo?[kNotificaObjectIdKey] as? String, let url = NSURL(string: urlString) else {
95 | return
96 | }
97 |
98 | guard let objectID = ReviewManager.managedObjectContext().persistentStoreCoordinator?.managedObjectIDForURIRepresentation(url) else {
99 | return
100 | }
101 |
102 | ReviewWindowController.show(objectID)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/AppReviews/Objective-C-BridgingHeader.h:
--------------------------------------------------------------------------------
1 | //
2 | // Objective-C-BridgingHeader.h
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-04.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | #import "PieChart.h"
10 |
11 |
--------------------------------------------------------------------------------
/AppReviews/PieChart/BackgroundView.h:
--------------------------------------------------------------------------------
1 | //
2 | // BackgroundView.h
3 | // SimplePieChart
4 | //
5 | // Created by subo on 14-4-23.
6 | // Copyright (c) 2014年 __MyCompanyName__. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface BackgroundView: NSView {
12 | NSColor *_backgroundColor;
13 | CGPoint _center;
14 | }
15 |
16 | @property (nonatomic,retain) NSColor *backgroundColor;
17 | @property (nonatomic,assign) CGPoint center;
18 |
19 | @end
20 |
--------------------------------------------------------------------------------
/AppReviews/PieChart/BackgroundView.m:
--------------------------------------------------------------------------------
1 | //
2 | // BackgroundView.m
3 | // SimplePieChart
4 | //
5 | // Created by subo on 14-4-23.
6 | // Copyright (c) 2014年 __MyCompanyName__. All rights reserved.
7 | //
8 |
9 | #import "BackgroundView.h"
10 | #import "NSColor+CGColor.h"
11 |
12 | @implementation BackgroundView
13 |
14 | @synthesize backgroundColor = _backgroundColor;
15 | @synthesize center = _center;
16 |
17 | - (void)setBackgroundColor:(NSColor *)backgroundColor {
18 | _backgroundColor = backgroundColor;
19 | self.layer.backgroundColor = _backgroundColor.CGColor;
20 | }
21 |
22 | @end
23 |
--------------------------------------------------------------------------------
/AppReviews/PieChart/NSColor+CGColor.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSColor+CGColor.h
3 | // SimplePieChart
4 | //
5 | // Created by subo on 14-4-23.
6 | // Copyright (c) 2014年 __MyCompanyName__. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface NSColor (CGColor)
12 |
13 | - (CGColorRef)CGColor;
14 |
15 | @end
16 |
--------------------------------------------------------------------------------
/AppReviews/PieChart/NSColor+CGColor.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSColor+CGColor.m
3 | // SimplePieChart
4 | //
5 | // Created by subo on 14-4-23.
6 | // Copyright (c) 2014年 __MyCompanyName__. All rights reserved.
7 | //
8 |
9 | #import "NSColor+CGColor.h"
10 |
11 | @implementation NSColor (CGColor)
12 |
13 | - (CGColorRef)CGColor
14 | {
15 | const NSInteger numberOfComponents = [self numberOfComponents];
16 | CGFloat components[numberOfComponents];
17 | CGColorSpaceRef colorSpace = [[self colorSpace] CGColorSpace];
18 |
19 | [self getComponents:(CGFloat *)&components];
20 |
21 | return (__bridge CGColorRef)(id)CFBridgingRelease(CGColorCreate(colorSpace, components));
22 | }
23 |
24 | @end
25 |
26 |
--------------------------------------------------------------------------------
/AppReviews/PieChart/PieChart.h:
--------------------------------------------------------------------------------
1 | //
2 | // PieChart.h
3 | // SimplePieChart
4 | //
5 | // Created by subo on 14-4-23.
6 | // Copyright (c) 2014年 __MyCompanyName__. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "BackgroundView.h"
11 |
12 | @class PieChart;
13 |
14 | @protocol PieChartDataSource
15 |
16 | @required
17 | - (NSUInteger)numberOfSlicesInPieChart:(PieChart *)pieChart;
18 | - (CGFloat)pieChart:(PieChart *)pieChart valueForSliceAtIndex:(NSUInteger)index;
19 |
20 | @optional
21 | - (NSColor *)pieChart:(PieChart *)pieChart colorForSliceAtIndex:(NSUInteger)index;
22 | - (NSString *)pieChart:(PieChart *)pieChart textForSliceAtIndex:(NSUInteger)index;
23 |
24 | @end
25 |
26 | @protocol PieChartDelegate
27 |
28 | @optional
29 | - (void)pieChart:(PieChart *)pieChart willSelectSliceAtIndex:(NSUInteger)index;
30 | - (void)pieChart:(PieChart *)pieChart didSelectSliceAtIndex:(NSUInteger)index;
31 | - (void)pieChart:(PieChart *)pieChart willDeselectSliceAtIndex:(NSUInteger)index;
32 | - (void)pieChart:(PieChart *)pieChart didDeselectSliceAtIndex:(NSUInteger)index;
33 | - (NSString *)pieChart:(PieChart *)pieChart toopTipStringAtIndex:(NSUInteger)index;
34 |
35 | @end
36 |
37 | @interface PieChart : BackgroundView {
38 |
39 | CGFloat _startPieAngle;
40 | CGFloat _animationSpeed;
41 | CGPoint _pieCenter;
42 | CGFloat _pieRadius;
43 | BOOL _showText;
44 | NSFont *_textFont;
45 | NSColor *_textColor;
46 | NSColor *_textShadowColor;
47 | CGFloat _textRadius;
48 | CGFloat _selectedSliceStroke;
49 | CGFloat _selectedSliceOffsetRadius;
50 | BOOL _showPercentage;
51 | BOOL _canSelect;
52 |
53 | @private
54 | NSInteger _selectedSliceIndex;
55 | //pie view, contains all slices
56 | BackgroundView *_pieView;
57 |
58 | //animation control
59 | NSTimer *_animationTimer;
60 | NSMutableArray *_animations;
61 |
62 | NSTrackingArea *_trackingArea;
63 | }
64 |
65 | @property (nonatomic,assign) id dataSource;
66 | @property (nonatomic,assign) id delegate;
67 | @property (nonatomic,assign) CGFloat startPieAngle;
68 | @property (nonatomic,assign) CGFloat animationSpeed;
69 | @property (nonatomic,assign) CGPoint pieCenter;
70 | @property (nonatomic,assign) CGFloat pieRadius; //半径
71 | @property (nonatomic,assign,getter = isShowText) BOOL showText;
72 | @property (nonatomic,retain) NSFont *textFont;
73 | @property (nonatomic,retain) NSColor *textColor;
74 | @property (nonatomic,retain) NSColor *textShadowColor;
75 | @property (nonatomic,assign) CGFloat textRadius;
76 | @property (nonatomic,assign) CGFloat selectedSliceStroke;
77 | @property (nonatomic,assign) CGFloat selectedSliceOffsetRadius;
78 | @property (nonatomic,assign,getter = isShowPercentage) BOOL showPercentage;
79 | @property (nonatomic,assign) BOOL canSelect;
80 |
81 | - (id)initWithFrame:(NSRect)frame Center:(CGPoint)center Radius:(CGFloat)radius;
82 | - (void)reloadData;
83 | - (void)setPieBackgroundColor:(NSColor *)color;
84 |
85 | - (void)setSliceSelectedAtIndex:(NSInteger)index;
86 | - (void)setSliceDeselectedAtIndex:(NSInteger)index;
87 |
88 | @end
89 |
--------------------------------------------------------------------------------
/AppReviews/PieChart/SliceLayer.h:
--------------------------------------------------------------------------------
1 | //
2 | // SliceLayer.h
3 | // TidyMyMusic
4 | //
5 | // Created by subo on 14-4-24.
6 | // Copyright (c) 2014年 __MyCompanyName__. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface SliceLayer : CAShapeLayer {
12 | CGFloat _value;
13 | CGFloat _percentage;
14 | double _startAngle;
15 | double _endAngle;
16 | BOOL _selected;
17 | NSString *_text;
18 | }
19 |
20 | @property (nonatomic, assign) CGFloat value;
21 | @property (nonatomic, assign) CGFloat percentage;
22 | @property (nonatomic, assign) double startAngle;
23 | @property (nonatomic, assign) double endAngle;
24 | @property (nonatomic, assign,getter = isSelected) BOOL selected;
25 | @property (nonatomic, copy) NSString *text;
26 |
27 | - (void)createArcAnimationForKey:(NSString *)key fromValue:(NSNumber *)from toValue:(NSNumber *)to Delegate:(id)delegate;
28 |
29 | @end
30 |
--------------------------------------------------------------------------------
/AppReviews/PieChart/SliceLayer.m:
--------------------------------------------------------------------------------
1 | //
2 | // SliceLayer.m
3 | // TidyMyMusic
4 | //
5 | // Created by subo on 14-4-24.
6 | // Copyright (c) 2014年 __MyCompanyName__. All rights reserved.
7 | //
8 |
9 | #import "SliceLayer.h"
10 |
11 | @implementation SliceLayer
12 |
13 | @synthesize text = _text;
14 | @synthesize value = _value;
15 | @synthesize percentage = _percentage;
16 | @synthesize startAngle = _startAngle;
17 | @synthesize endAngle = _endAngle;
18 | @synthesize selected = _selected;
19 |
20 | + (BOOL)needsDisplayForKey:(NSString *)key
21 | {
22 | if ([key isEqualToString:@"startAngle"] || [key isEqualToString:@"endAngle"]) {
23 | return YES;
24 | }
25 | else {
26 | return [super needsDisplayForKey:key];
27 | }
28 | }
29 |
30 | - (id)initWithLayer:(id)layer
31 | {
32 | if (self = [super initWithLayer:layer])
33 | {
34 | if ([layer isKindOfClass:[SliceLayer class]]) {
35 | self.startAngle = [(SliceLayer *)layer startAngle];
36 | self.endAngle = [(SliceLayer *)layer endAngle];
37 | }
38 | }
39 | return self;
40 | }
41 |
42 | - (NSString*)description
43 | {
44 | return [NSString stringWithFormat:@"value:%f, percentage:%0.0f, start:%f, end:%f", _value, _percentage, _startAngle/M_PI*180, _endAngle/M_PI*180];
45 | }
46 |
47 | - (void)createArcAnimationForKey:(NSString *)key fromValue:(NSNumber *)from toValue:(NSNumber *)to Delegate:(id)delegate
48 | {
49 | CABasicAnimation *arcAnimation = [CABasicAnimation animationWithKeyPath:key];
50 | NSNumber *currentAngle = [[self presentationLayer] valueForKey:key];
51 | if(!currentAngle) currentAngle = from;
52 | [arcAnimation setFromValue:currentAngle];
53 | [arcAnimation setToValue:to];
54 | [arcAnimation setDelegate:delegate];
55 | [arcAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];
56 | [self addAnimation:arcAnimation forKey:key];
57 | [self setValue:to forKey:key];
58 | }
59 |
60 | @end
61 |
--------------------------------------------------------------------------------
/AppReviews/ReviewArrayController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewArrayController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-11.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class ReviewArrayController: NSArrayController {
12 |
13 | var application: Application? {
14 | didSet {
15 | if let application = self.application {
16 | self.filterPredicate = NSPredicate(format: "application = %@", application)
17 | }
18 | }
19 | }
20 |
21 | override func awakeFromNib() {
22 | super.awakeFromNib()
23 |
24 | sortDescriptors = [
25 | NSSortDescriptor(key: "version", ascending: false, selector: Selector("compareVersion:")),
26 | NSSortDescriptor(key: "createdAt", ascending: false)
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/AppReviews/ReviewCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewCellView.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-12.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 | import EDStarRating
11 |
12 | class ReviewCellView: NSTableCellView {
13 |
14 | @IBOutlet weak var starRating: EDStarRating?
15 | private var kvoContext = 0
16 |
17 | deinit {
18 | removeObserver(self, forKeyPath: "objectValue", context: &kvoContext)
19 | }
20 |
21 | required init?(coder: NSCoder) {
22 | super.init(coder: coder)
23 |
24 | addObserver(self, forKeyPath: "objectValue", options: .New, context: &kvoContext)
25 | }
26 |
27 | override func awakeFromNib() {
28 | super.awakeFromNib()
29 |
30 | if let starRating = starRating {
31 | starRating.starImage = NSImage(named: "star")
32 | starRating.starHighlightedImage = NSImage(named: "star-highlighted")
33 | starRating.maxRating = 5
34 | starRating.delegate = self
35 | starRating.horizontalMargin = 5
36 | starRating.displayMode = UInt(EDStarRatingDisplayAccurate)
37 | starRating.rating = 3.5
38 | }
39 | }
40 |
41 | override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String: AnyObject]?, context: UnsafeMutablePointer) {
42 | if context == &kvoContext {
43 | if let starRating = starRating, objectValue = objectValue as? NSManagedObject {
44 | starRating.bind("rating", toObject: objectValue, withKeyPath: "rating", options: nil)
45 | }
46 | }
47 | else {
48 | super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
49 | }
50 | }
51 |
52 | }
53 |
54 | // MARK: EDStarRatingProtocol
55 |
56 | extension ReviewCellView: EDStarRatingProtocol {
57 |
58 | }
--------------------------------------------------------------------------------
/AppReviews/ReviewPieChartController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PieChart.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-04.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class ReviewPieChartController: NSViewController {
12 |
13 | @IBOutlet weak var pieChart: PieChart?
14 |
15 | var slices = [Float]()
16 | var sliceColors: [NSColor]!
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 |
21 | sliceColors = [NSColor.reviewRed(), NSColor.reviewOrange(), NSColor.reviewYellow(), NSColor.reviewGreen(), NSColor.reviewBlue()]
22 |
23 | if let pieChart = pieChart {
24 | pieChart.dataSource = self
25 | pieChart.delegate = self
26 | pieChart.pieCenter = CGPointMake(240, 240)
27 | pieChart.showPercentage = false
28 | }
29 | }
30 | }
31 |
32 | extension ReviewPieChartController: PieChartDataSource {
33 |
34 | func numberOfSlicesInPieChart(pieChart: PieChart!) -> UInt {
35 | return UInt(slices.count)
36 | }
37 |
38 | func pieChart(pieChart: PieChart!, valueForSliceAtIndex index: UInt) -> CGFloat {
39 | return CGFloat(slices[Int(index)])
40 | }
41 |
42 | func pieChart(pieChart: PieChart!, colorForSliceAtIndex index: UInt) -> NSColor! {
43 | return sliceColors[Int(index) % sliceColors.count]
44 | }
45 |
46 | func pieChart(pieChart: PieChart!, textForSliceAtIndex index: UInt) -> String! {
47 | return (NSString(format: NSLocalizedString("%i Stars", comment: "review.slice.tooltip"), index + 1) as String)
48 | }
49 | }
50 |
51 | extension ReviewPieChartController: PieChartDelegate {
52 | func pieChart(pieChart: PieChart!, toopTipStringAtIndex index: UInt) -> String! {
53 | let number = slices[Int(index)]
54 | return (NSString(format: NSLocalizedString("Number of ratings: ", comment: "review.slice.tooltip"), number) as String)
55 | }
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/AppReviews/ReviewSplitViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewSplitViewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-22.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class ReviewSplitViewController: NSSplitViewController {
12 |
13 | var application: Application? {
14 | didSet {
15 | reviewMenuViewController?.application = application
16 | reviewViewController?.application = application
17 | }
18 | }
19 |
20 | var reviewMenuViewController: ReviewMenuViewController?
21 | var reviewViewController: ReviewViewController?
22 |
23 | var menuSplitViewItem: NSSplitViewItem {
24 | return splitViewItems[0]
25 | }
26 |
27 | override func viewDidLoad() {
28 | super.viewDidLoad()
29 |
30 | if let reviewMenuViewController = menuSplitViewItem.viewController as? ReviewMenuViewController {
31 | self.reviewMenuViewController = reviewMenuViewController
32 | self.reviewMenuViewController?.application = application
33 | }
34 | if let reviewViewController = menuSplitViewItem.viewController as? ReviewViewController {
35 | self.reviewViewController = reviewViewController
36 | self.reviewViewController?.application = application
37 | }
38 |
39 | menuSplitViewItem.animator().collapsed = NSUserDefaults.review_isLeftMenuCollapsed()
40 | }
41 |
42 | override func viewDidDisappear() {
43 | super.viewDidDisappear()
44 | NSUserDefaults.review_setLeftMenuCollapsed(menuSplitViewItem.collapsed)
45 | }
46 |
47 | func toggleLeftMenu() {
48 | menuSplitViewItem.animator().collapsed = !menuSplitViewItem.collapsed
49 | }
50 | }
--------------------------------------------------------------------------------
/AppReviews/ReviewViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-08.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class ReviewViewController: NSViewController {
12 |
13 | @IBOutlet weak var tableView: NSTableView?
14 | @IBOutlet var reviewArrayController: ReviewArrayController?
15 |
16 | var managedObjectContext: NSManagedObjectContext!
17 |
18 | var selectedCell: NSTableCellView?
19 |
20 | var selectedReview: Review? {
21 | if tableView?.selectedRow >= 0 && tableView?.selectedRow < reviewArrayController?.arrangedObjects.count {
22 | return reviewArrayController?.arrangedObjects[tableView!.selectedRow] as? Review
23 | } else {
24 | return nil
25 | }
26 | }
27 |
28 | var application: Application? {
29 | didSet {
30 | reviewArrayController?.application = application
31 | if let application = application {
32 | ReviewManager.appUpdater().resetNewReviewsCountForApplication(application.objectID)
33 | }
34 | tableView?.reloadData()
35 | }
36 | }
37 | // MARK: - Init & teardown
38 |
39 | required init?(coder: NSCoder) {
40 | super.init(coder: coder)
41 | managedObjectContext = ReviewManager.managedObjectContext()
42 |
43 | // Resize TableViewCellHeights when View is resized.
44 | _ = NSNotificationCenter.defaultCenter().addObserverForName(NSViewFrameDidChangeNotification, object: nil, queue: nil) { [weak self] notification in
45 | if let tableView = notification.object as? NSTableView {
46 | let visibleRows = tableView.rowsInRect(self!.view.frame)
47 | NSAnimationContext.beginGrouping()
48 | NSAnimationContext.currentContext().duration = 0
49 | tableView.noteHeightOfRowsWithIndexesChanged(NSIndexSet(indexesInRange: visibleRows))
50 | NSAnimationContext.endGrouping()
51 | }
52 | }
53 | }
54 |
55 | }
56 |
57 | // MARK: NSTableViewDelegate
58 |
59 | extension ReviewViewController: NSTableViewDelegate {
60 |
61 | func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
62 |
63 | let review = reviewArrayController?.arrangedObjects[row] as? Review
64 | let height = review?.content.size(tableView.frame.size.width - 85, font: NSFont.systemFontOfSize(13)).height ?? 0
65 |
66 | return height + 80
67 | }
68 |
69 | func tableViewSelectionDidChange(notification: NSNotification) {
70 | if let row = notification.object?.selectedRow {
71 | selectedCell = notification.object?.viewAtColumn(0, row: row, makeIfNecessary: false) as? NSTableCellView
72 | }
73 | }
74 | }
75 |
76 | // MARK: Actions
77 |
78 | extension ReviewViewController {
79 |
80 | @IBAction func shareSelectedReview(sender: AnyObject?) {
81 | if let review = self.selectedReview, let textField = self.selectedCell?.textField {
82 | let sharingServicePicker = NSSharingServicePicker(items: [review.toString()])
83 | sharingServicePicker.delegate = self
84 | sharingServicePicker.showRelativeToRect(textField.bounds, ofView: textField, preferredEdge: NSRectEdge.MinY)
85 | } else {
86 | let alert = NSAlert()
87 | alert.messageText = NSLocalizedString("Select a review to share.", comment: "review.share.nothingSelected")
88 | alert.beginSheetModalForWindow(self.view.window!, completionHandler:nil)
89 | }
90 | }
91 |
92 | @IBAction func copyToClipBoardSelectedReview(sender: AnyObject?) {
93 | if let review = self.selectedReview {
94 | let pasteBoard = NSPasteboard.generalPasteboard()
95 | pasteBoard.clearContents()
96 | pasteBoard.writeObjects([review.toString()])
97 | }
98 | }
99 |
100 | @IBAction func openInItunesSelectedReview(sender: AnyObject?) {
101 | if let review = self.selectedReview {
102 | if let url = NSURL(string: review.uri) {
103 | NSWorkspace.sharedWorkspace().openURL(url)
104 | }
105 | }
106 | }
107 |
108 | @IBAction func saveSelectedReview(sender: AnyObject?) {
109 | if let review = self.selectedReview {
110 | let savePanel = NSSavePanel()
111 | savePanel.title = review.title
112 | savePanel.nameFieldStringValue = review.title
113 | savePanel.allowedFileTypes = [kUTTypeText as String]
114 | let result = savePanel.runModal()
115 | if result != NSFileHandlingPanelCancelButton {
116 | if let url = savePanel.URL {
117 | do {
118 | try review.toString().writeToURL(url, atomically: true, encoding: NSUTF8StringEncoding)
119 | } catch let error as NSError {
120 | print(error)
121 | }
122 | }
123 | }
124 | }
125 | }
126 | }
127 |
128 | // MARK: - NSSharingServicePickerDelegate
129 |
130 | extension ReviewViewController: NSSharingServicePickerDelegate {
131 |
132 | }
133 |
134 |
--------------------------------------------------------------------------------
/AppReviews/ReviewWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewWindowController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-17.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class ReviewWindowController: NSWindowController {
12 | var reviewController : ReviewViewController?
13 | var managedObjectContext: NSManagedObjectContext!
14 | @IBOutlet weak var automaticUpdate: NSMenuItem?
15 | @IBOutlet weak var shareButton: NSButton?
16 |
17 | var application: Application? {
18 | didSet {
19 | if let application = application {
20 | window?.title = application.trackName
21 | automaticUpdate?.state = application.settings.automaticUpdate ? NSOnState: NSOffState
22 |
23 | if let reviewSplitViewController = contentViewController as? ReviewSplitViewController {
24 | reviewSplitViewController.application = application
25 | }
26 | }
27 | }
28 | }
29 |
30 | var objectId: NSManagedObjectID? {
31 | didSet {
32 | if oldValue != objectId {
33 | let context = ReviewManager.managedObjectContext()
34 | if let objectId = objectId {
35 | do {
36 | application = try context.existingObjectWithID(objectId) as? Application
37 | } catch let error as NSError {
38 | print(error)
39 | } catch {
40 | fatalError()
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | // MARK: - Init & teardown
48 |
49 | class func show(objectId: NSManagedObjectID) {
50 | let appdelegate = NSApplication.sharedApplication().delegate as! AppDelegate
51 | let windowController = appdelegate.reviewsWindowController
52 | windowController.managedObjectContext = ReviewManager.managedObjectContext()
53 | windowController.objectId = objectId
54 | windowController.showWindow(self)
55 | NSApp.activateIgnoringOtherApps(true)
56 | }
57 |
58 | // MARK: - Loading
59 |
60 | override func awakeFromNib() {
61 | super.awakeFromNib()
62 |
63 | // Update frame manually
64 | let frame = self.window!.frame
65 | self.window?.setFrame(NSRect(x: frame.origin.x, y: frame.origin.y, width: 800, height: 700), display: true)
66 |
67 | if let reviewSplitViewController = contentViewController as? ReviewSplitViewController {
68 | reviewSplitViewController.application = application
69 | reviewController = reviewSplitViewController.reviewViewController
70 | }
71 |
72 | self.shareButton!.sendActionOn(Int(NSEventMask.LeftMouseDownMask.rawValue))
73 |
74 | // Register Keyboard shortcuts.
75 | NSEvent.addLocalMonitorForEventsMatchingMask(NSEventMaskFromType(.KeyDown), handler: { [weak self] (event: NSEvent!) -> NSEvent! in
76 |
77 | let rChar: UInt16 = 15
78 | let iChar: UInt16 = 34
79 | let cChar: UInt16 = 8
80 | let sChar: UInt16 = 1
81 | let fChar: UInt16 = 3
82 | let aChar: UInt16 = 0
83 |
84 | if event.modifierFlags.intersect(NSEventModifierFlags.CommandKeyMask) != [] {
85 | switch event.keyCode {
86 | case rChar:
87 | self?.refreshApplication(nil)
88 | case aChar:
89 | self?.openApplications(nil)
90 | case iChar:
91 | if (event.modifierFlags.intersect(NSEventModifierFlags.ShiftKeyMask) == []) {
92 | self?.reviewController?.openInItunesSelectedReview(nil)
93 | }
94 | case cChar:
95 | self?.reviewController?.copyToClipBoardSelectedReview(nil)
96 | case sChar:
97 | self?.reviewController?.saveSelectedReview(nil)
98 | case fChar:
99 | self?.reviewController?.shareSelectedReview(nil)
100 | case 18, 19 , 20, 21, 22, 23, 24, 25:
101 | let key = Int(event.keyCode) - 18
102 | let appdelegate = NSApplication.sharedApplication().delegate as! AppDelegate
103 | let menuItems = appdelegate.statusMenuController.statusItem.menu?.itemArray
104 | if menuItems?.count > key {
105 | if let menuItem = menuItems?[key], let application = menuItem.representedObject as? Application {
106 | ReviewWindowController.show(application.objectID)
107 | }
108 | }
109 | default:
110 | break
111 | }
112 | }
113 |
114 | return event
115 | })
116 | }
117 | }
118 |
119 | // MARK: - Actions
120 |
121 | extension ReviewWindowController {
122 |
123 | func openApplications(sender: AnyObject?) {
124 | let appdelegate = NSApplication.sharedApplication().delegate as! AppDelegate
125 | let windowController = appdelegate.applicationWindowController
126 | windowController.showWindow(self)
127 | NSApp.activateIgnoringOtherApps(true)
128 | }
129 |
130 | @IBAction func refreshApplication(sender: AnyObject?) {
131 | if let application = application {
132 | ReviewManager.appUpdater().fetchReviewsForApplication(application.objectID)
133 | }
134 | }
135 |
136 | @IBAction func automaticUpdateDidChangeState(sender: AnyObject) {
137 |
138 | guard let menuItem = sender as? NSMenuItem, objectId = application?.objectID else { return }
139 |
140 | let newState = !Bool(menuItem.state)
141 | menuItem.state = newState ? NSOnState: NSOffState;
142 | DatabaseHandler.saveDataInContext({ (context) -> Void in
143 | do {
144 | if let application = try context.existingObjectWithID(objectId) as? Application {
145 | application.settings.automaticUpdate = newState
146 | }
147 | } catch let error as NSError {
148 | print(error)
149 | } catch {
150 | fatalError()
151 | }
152 | })
153 | }
154 |
155 | @IBAction func openInAppstore(objects: AnyObject?) {
156 | let itunesUrl = "http://itunes.apple.com/app/id" + (application?.trackId ?? "")
157 | if let url = NSURL(string: itunesUrl) {
158 | NSWorkspace.sharedWorkspace().openURL(url)
159 | }
160 | }
161 |
162 | @IBAction func shareButtonClicked(sender: AnyObject?) {
163 | if let reviewSplitController = contentViewController as? ReviewSplitViewController {
164 | reviewSplitController.reviewViewController?.shareSelectedReview(sender)
165 | }
166 | }
167 |
168 | @IBAction func sideBarButtonClicked(sender: AnyObject?) {
169 | guard let reviewSplitViewController = contentViewController as? ReviewSplitViewController else { return }
170 | reviewSplitViewController.toggleLeftMenu()
171 | }
172 |
173 | @IBAction func exportReviewsClicked(sender: AnyObject?) {
174 |
175 | guard let application = application else { return }
176 | guard let reviewController = contentViewController as? ReviewSplitViewController else { return }
177 | guard let reviews = reviewController.reviewViewController?.reviewArrayController?.arrangedObjects as? [Review] else { return }
178 |
179 | var stringToExport = ""
180 | for review in reviews {
181 | stringToExport += "\n\n" + review.toString() + "\n\n_________________________________"
182 | }
183 |
184 | let savePanel = NSSavePanel()
185 | savePanel.allowedFileTypes = [kUTTypeText as String]
186 | let result = savePanel.runModal()
187 | savePanel.title = application.trackName
188 | savePanel.nameFieldStringValue = application.trackName
189 |
190 | if result != NSFileHandlingPanelCancelButton, let url = savePanel.URL {
191 | do {
192 | try stringToExport.writeToURL(url, atomically: true, encoding: NSUTF8StringEncoding)
193 | } catch let error as NSError {
194 | let alert = NSAlert()
195 | alert.messageText = (error.localizedDescription)
196 | alert.beginSheetModalForWindow(window!, completionHandler:nil)
197 | }
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/AppReviews/SearchViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationSearchViewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-13.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import SwiftyJSON
11 |
12 | protocol SearchViewControllerDelegate {
13 | func searchViewController(searchViewController: SearchViewController, didSelectApplication application: JSON)
14 | func searchViewControllerDidCancel(searchViewController: SearchViewController)
15 | }
16 |
17 | enum SearchViewControllerState {
18 | case Idle
19 | case Loading
20 | }
21 |
22 | class SearchViewController: NSViewController {
23 |
24 | @IBOutlet weak var tableView: NSTableView!
25 | @IBOutlet weak var progressIndicator: NSProgressIndicator!
26 |
27 | var items = [JSON]()
28 | var delegate: SearchViewControllerDelegate?
29 | var state: SearchViewControllerState = .Idle {
30 | didSet {
31 | switch self.state {
32 | case .Idle:
33 | progressIndicator.stopAnimation(nil)
34 | case .Loading:
35 | progressIndicator.startAnimation(nil)
36 | }
37 | }
38 | }
39 |
40 | override func viewDidLoad() {
41 | super.viewDidLoad()
42 | tableView.target = self
43 | tableView.doubleAction = Selector("doubleClickedCell:")
44 | }
45 | }
46 |
47 | // MARK: - Actions
48 |
49 | extension SearchViewController {
50 |
51 | func doubleClickedCell(object: AnyObject) {
52 | if let rowNumber = tableView?.selectedRow {
53 | if rowNumber < items.count {
54 | let application = items[rowNumber]
55 | delegate?.searchViewController(self, didSelectApplication: application)
56 | }
57 | }
58 | }
59 |
60 | @IBAction func cancelButtonClicked(sender: AnyObject) {
61 | delegate?.searchViewControllerDidCancel(self)
62 | }
63 | }
64 |
65 | // MARK: - NSTableViewDataSource
66 |
67 | extension SearchViewController: NSTableViewDataSource {
68 |
69 | func numberOfRowsInTableView(aTableView: NSTableView) -> Int {
70 | return items.count
71 | }
72 |
73 | func tableView(tableView: NSTableView, viewForTableColumn: NSTableColumn, row: Int) -> NSView {
74 | let cell = tableView.makeViewWithIdentifier(kApplicationCellIdentifier, owner: self) as! ApplicationCellView
75 | let application = items[row]
76 | cell.textField?.stringValue = application.trackName ?? ""
77 | cell.authorTextField?.stringValue = application.sellerName ?? ""
78 |
79 | if let urlString = application.artworkUrl60 {
80 | if let url = NSURL(string: urlString) {
81 | cell.imageView?.setImageWithUrl(url, placeHolderImage: nil)
82 | }
83 | }
84 |
85 | return cell;
86 | }
87 | }
88 |
89 | // MARK: NSResponder
90 |
91 | extension SearchViewController {
92 | override func cancelOperation(sender: AnyObject?) {
93 | delegate?.searchViewControllerDidCancel(self)
94 | }
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/AppReviews/StatusMenuController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusMenu.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-16.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class StatusMenuController: NSObject {
12 |
13 | var statusItem: NSStatusItem!
14 | var applications = [Application]()
15 | var newReviews = [Int]()
16 | var applicationArrayController: ApplicationArrayController!
17 | private var kvoContext = 0
18 |
19 | // MARK: - Init & teardown
20 |
21 | deinit {
22 | removeObserver(self, forKeyPath: "applications", context: &kvoContext)
23 | }
24 |
25 | override init() {
26 | super.init()
27 | statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1) // NSVariableStatusItemLength
28 | statusItem.image = NSImage(named: "stausBarIcon")
29 | statusItem.alternateImage = NSImage(named: "stausBarIcon")
30 | statusItem.highlightMode = true
31 |
32 | applicationArrayController = ApplicationArrayController(content: nil)
33 | applicationArrayController.managedObjectContext = ReviewManager.managedObjectContext()
34 | applicationArrayController.entityName = kEntityNameApplication
35 | do {
36 | try applicationArrayController.fetchWithRequest(nil, merge: true)
37 | } catch let error as NSError {
38 | print(error)
39 | }
40 |
41 | bind("applications", toObject: applicationArrayController, withKeyPath: "arrangedObjects", options: nil)
42 |
43 | addObserver(self, forKeyPath: "applications", options: .New, context: &kvoContext)
44 |
45 | _ = NSNotificationCenter.defaultCenter().addObserverForName(kDidUpdateApplicationNotification, object: nil, queue: nil) { notification in
46 | }
47 |
48 | _ = NSNotificationCenter.defaultCenter().addObserverForName(kDidUpdateApplicationSettingsNotification, object: nil, queue: nil) { [weak self] notification in
49 | self?.updateMenu()
50 | }
51 |
52 | updateMenu()
53 | }
54 |
55 | // MARK: - Handling menu items
56 |
57 | func updateMenu() {
58 | let menu = NSMenu()
59 |
60 | var newReviews = false
61 |
62 | var idx = 1
63 | for application in applications {
64 |
65 | // application.addObserver(self, forKeyPath: "settings.newReviews", options: .New, context: &kvoContext)
66 | var title = application.trackName
67 |
68 | if application.settings.newReviews.integerValue > 0 {
69 | newReviews = true
70 | title = title + " (" + String(application.settings.newReviews.integerValue) + ")"
71 | }
72 |
73 | let shortKey = idx < 10 ? String(idx) : ""
74 | let menuItem = NSMenuItem(title: title, action: Selector("openReviewsForApp:"), keyEquivalent: shortKey)
75 |
76 | menuItem.representedObject = application
77 | menuItem.target = self
78 | menu.addItem(menuItem)
79 | idx++
80 | }
81 |
82 | if (applications.count > 0) {
83 | menu.addItem(NSMenuItem.separatorItem())
84 | }
85 |
86 | let menuItemApplications = NSMenuItem(title: NSLocalizedString("Add / Remove Applications", comment: "statusbar.menu.applications"), action: Selector("openApplications:"), keyEquivalent: "a")
87 | let menuItemAbout = NSMenuItem(title: NSLocalizedString("About Appstore Reviews", comment: "statusbar.menu.about"), action: Selector("openAbout:"), keyEquivalent: "")
88 | let menuItemProvidFeedback = NSMenuItem(title: NSLocalizedString("Provide Feedback...", comment: "statusbar.menu.feedback"), action: Selector("openFeedback:"), keyEquivalent: "")
89 |
90 | let menuItemQuit = NSMenuItem(title: NSLocalizedString("Quit", comment: "statusbar.menu.quit"), action: Selector("quit:"), keyEquivalent: "q")
91 |
92 | let menuItemLaunchAtStartup = NSMenuItem(title: NSLocalizedString("Launch at startup", comment: "statusbar.menu.startup"), action: Selector("launchAtStartUpToggle:"), keyEquivalent: "")
93 | menuItemLaunchAtStartup.state = NSApplication.shouldLaunchAtStartup() ? NSOnState : NSOffState
94 |
95 | menuItemApplications.target = self
96 | menuItemAbout.target = self
97 | menuItemQuit.target = self
98 | menuItemProvidFeedback.target = self
99 | menuItemLaunchAtStartup.target = self
100 |
101 | menu.addItem(menuItemApplications)
102 | menu.addItem(NSMenuItem.separatorItem())
103 | menu.addItem(menuItemLaunchAtStartup)
104 | menu.addItem(menuItemProvidFeedback)
105 | menu.addItem(NSMenuItem.separatorItem())
106 | menu.addItem(menuItemAbout)
107 | menu.addItem(menuItemQuit)
108 |
109 | statusItem.menu = menu;
110 |
111 | if newReviews {
112 | statusItem.image = NSImage(named: "stausBarIconHappy")
113 | statusItem.alternateImage = NSImage(named: "stausBarIconHappy")
114 | } else {
115 | statusItem.image = NSImage(named: "stausBarIcon")
116 | statusItem.alternateImage = NSImage(named: "stausBarIcon")
117 | }
118 | }
119 | }
120 |
121 | // MARK: - Actions
122 |
123 | extension StatusMenuController {
124 |
125 | func openReviewsForApp(sender: AnyObject?) {
126 | if let menuItem = sender as? NSMenuItem {
127 | if let application = menuItem.representedObject as? Application {
128 | ReviewWindowController.show(application.objectID)
129 | }
130 | }
131 | }
132 |
133 | func openAbout(sender: AnyObject?) {
134 | let appdelegate = NSApplication.sharedApplication().delegate as! AppDelegate
135 | let windowController = appdelegate.aboutWindowController
136 | windowController.showWindow(self)
137 | NSApp.activateIgnoringOtherApps(true)
138 | }
139 |
140 | func openApplications(sender: AnyObject?) {
141 | let appdelegate = NSApplication.sharedApplication().delegate as! AppDelegate
142 | let windowController = appdelegate.applicationWindowController
143 | windowController.showWindow(self)
144 | NSApp.activateIgnoringOtherApps(true)
145 | }
146 |
147 | func openFeedback(sender: AnyObject?) {
148 | NSWorkspace.sharedWorkspace().openURL(NSURL(string: "http://knutigro.github.io/apps/app-reviews/#Feedback")!)
149 | }
150 |
151 | func launchAtStartUpToggle(sender : AnyObject?) {
152 | if let menu = sender as? NSMenuItem {
153 | NSApplication.toggleShouldLaunchAtStartup()
154 | menu.state = NSApplication.shouldLaunchAtStartup() ? NSOnState : NSOffState
155 | }
156 | }
157 |
158 | func quit(sender: AnyObject?) {
159 | NSApplication.sharedApplication().terminate(sender)
160 | }
161 | }
162 |
163 | // MARK: - KVO
164 |
165 | extension StatusMenuController {
166 |
167 | override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String: AnyObject]?, context: UnsafeMutablePointer) {
168 | if context == &kvoContext {
169 | // print("observeValueForKeyPath: " + keyPath + "change: \(change)" )
170 | // updateMenu()
171 | }
172 | else {
173 | super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
174 | }
175 | }
176 |
177 | }
--------------------------------------------------------------------------------
/AppReviews/String+Size.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Size.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-23.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 |
12 | extension String {
13 |
14 | func size(width: CGFloat, font: NSFont) -> NSSize {
15 | let range = NSMakeRange(0, (self as NSString).length)
16 | var size = NSMakeSize(width, CGFloat(MAXFLOAT))
17 | let textStorage = NSTextStorage(string: self)
18 | let textContainer = NSTextContainer(containerSize: size)
19 | let layoutManager = NSLayoutManager()
20 | layoutManager.addTextContainer(textContainer)
21 | textStorage.addLayoutManager(layoutManager)
22 | textStorage.addAttribute(NSFontAttributeName, value: font, range: range)
23 | textContainer.lineFragmentPadding = 0.0
24 | layoutManager.glyphRangeForTextContainer(textContainer)
25 |
26 | size.height = layoutManager.usedRectForTextContainer(textContainer).size.height
27 |
28 | return size
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/AppReviews/TableImageCellTransformer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableImageCellTransformer.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-14.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | @objc(TableImageCellTransformer)
12 | class TableImageCellTransformer: NSValueTransformer{
13 |
14 | override class func transformedValueClass() -> AnyClass {
15 | return NSImage.self
16 | }
17 |
18 | override func transformedValue(value: AnyObject!) -> AnyObject? {
19 | guard let value = value as? String, let url = NSURL(string: value) else {
20 | return nil
21 | }
22 |
23 | return NSImage(contentsOfURL: url)
24 | }
25 | }
--------------------------------------------------------------------------------
/AppReviewsTests/AppReviewsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // App ReviewsTests.swift
3 | // App ReviewsTests
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-08.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import XCTest
11 | import SwiftyJSON
12 |
13 | class AppstoreReviewsTests: XCTestCase {
14 |
15 | override func setUp() {
16 | super.setUp()
17 | // Put setup code here. This method is called before the invocation of each test method in the class.
18 | }
19 |
20 | override func tearDown() {
21 | // Put teardown code here. This method is called after the invocation of each test method in the class.
22 | super.tearDown()
23 | }
24 |
25 | func testPerformanceExample() {
26 | // This is an example of a performance test case.
27 | measureBlock() {
28 | // Put the code you want to measure the time of here.
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/AppReviewsTests/AppVersionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppVersionTests.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-15.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import XCTest
11 |
12 | class AppVersionTests: XCTestCase {
13 |
14 | func testVersionSortDecriptor() {
15 | // This is an example of a functional test case.
16 |
17 | let version0 = "0.2.4"
18 | let version1 = "1.2.3"
19 | let version2 = "1.2"
20 | let version3 = "1.beta3.4"
21 | let version4 = "1.3.4.5"
22 | let version5 = "2.0"
23 | let version6 = "2.beta1"
24 |
25 | let test1 = [version0, version1, version2, version3, version4, version0, version5, version6]
26 | let test2 = [version6, version5, version4, version3, version2, version1, version0, version0]
27 |
28 | let sortDescriptors = [NSSortDescriptor(key: "self", ascending: true, selector: Selector("compareVersion:"))]
29 |
30 | let sortedArray1 = NSArray(array: test1).sortedArrayUsingDescriptors(sortDescriptors)
31 | let sortedArray2 = NSArray(array: test2).sortedArrayUsingDescriptors(sortDescriptors)
32 |
33 | XCTAssertEqual(sortedArray1.first as? String, sortedArray2.first as? String, "The two first versions should be the same")
34 | XCTAssertEqual(sortedArray1.last as? String, version6, "The biggest version is 2.beta1")
35 |
36 | print("sortedArray1 \(sortedArray1)")
37 | print("sortedArray2 \(sortedArray2)")
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/AppReviewsTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | com.cocmoc.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/AppReviewsTests/ItunesUrlHandlerTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItunesUrlHandlerTest.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-15.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | import Cocoa
12 | import XCTest
13 | import SwiftyJSON
14 |
15 | class ItunesUrlHandlerTest: XCTestCase {
16 |
17 | var urlHandler: ItunesUrlHandler!
18 | let kInitialUrl = "https://itunes.apple.com/rss/customerreviews/id=123/json"
19 | var reviewJSON: JSON?
20 |
21 | override func setUp() {
22 | super.setUp()
23 | // Put setup code here. This method is called before the invocation of each test method in the class.
24 |
25 | urlHandler = ItunesUrlHandler(apId: "123", storeId: nil)
26 |
27 | if let path = NSBundle(forClass: ItunesUrlHandlerTest.self).pathForResource("reviews", ofType: "json") {
28 | do {
29 | let string = try NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding)
30 | if let dataFromString = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) {
31 | reviewJSON = JSON(data: dataFromString)
32 | }
33 | } catch {
34 | XCTFail("contentsOfFile did fail")
35 | }
36 | }
37 | XCTAssertNotNil(reviewJSON)
38 | }
39 |
40 | func testIfFeedExist() {
41 | guard let reviewJSON = self.reviewJSON else {
42 | return
43 | }
44 |
45 | XCTAssertNotNil(reviewJSON.itunesFeed)
46 | }
47 |
48 | func testIfReviewsExist() {
49 | guard let reviewJSON = self.reviewJSON else {
50 | return
51 | }
52 |
53 | XCTAssertNotNil(reviewJSON.itunesReviews)
54 | XCTAssertGreaterThan(reviewJSON.itunesReviews.count, 0)
55 | }
56 |
57 | func testIfLinkExist() {
58 | guard let reviewJSON = self.reviewJSON else {
59 | return
60 | }
61 | XCTAssertGreaterThan(reviewJSON.itunesFeedLinks.count, 0)
62 | }
63 |
64 | func testInitialUrl() {
65 | // pages.append(ItunesPage(url: initialUrl, page: 0))
66 |
67 | XCTAssertEqual(urlHandler.initialUrl, kInitialUrl, "There should be inital url")
68 | }
69 |
70 | func testPrecedingUrlUrl() {
71 | // if let json = json {
72 | // urlHandler.updateWithJSON(json["feed"]["link"].arrayValue)
73 | // } else {
74 | // }
75 |
76 | print("nextUrl \(urlHandler.nextUrl)")
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Crashlytics:
--------------------------------------------------------------------------------
1 | Versions/Current/Crashlytics
--------------------------------------------------------------------------------
/Crashlytics.framework/Headers:
--------------------------------------------------------------------------------
1 | Versions/Current/Headers
--------------------------------------------------------------------------------
/Crashlytics.framework/Modules:
--------------------------------------------------------------------------------
1 | Versions/Current/Modules
--------------------------------------------------------------------------------
/Crashlytics.framework/Resources:
--------------------------------------------------------------------------------
1 | Versions/Current/Resources
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Crashlytics:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Crashlytics.framework/Versions/A/Crashlytics
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Headers/ANSCompatibility.h:
--------------------------------------------------------------------------------
1 | //
2 | // ANSCompatibility.h
3 | // AnswersKit
4 | //
5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
6 | //
7 |
8 | #pragma once
9 |
10 | #if !__has_feature(nullability)
11 | #define nonnull
12 | #define nullable
13 | #define _Nullable
14 | #define _Nonnull
15 | #endif
16 |
17 | #ifndef NS_ASSUME_NONNULL_BEGIN
18 | #define NS_ASSUME_NONNULL_BEGIN
19 | #endif
20 |
21 | #ifndef NS_ASSUME_NONNULL_END
22 | #define NS_ASSUME_NONNULL_END
23 | #endif
24 |
25 | #if __has_feature(objc_generics)
26 | #define ANS_GENERIC_NSARRAY(type) NSArray
27 | #define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
28 | #else
29 | #define ANS_GENERIC_NSARRAY(type) NSArray
30 | #define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
31 | #endif
32 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Headers/CLSAttributes.h:
--------------------------------------------------------------------------------
1 | //
2 | // CLSAttributes.h
3 | // Crashlytics
4 | //
5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
6 | //
7 |
8 | #pragma once
9 |
10 | #define CLS_DEPRECATED(x) __attribute__ ((deprecated(x)))
11 |
12 | #if !__has_feature(nullability)
13 | #define nonnull
14 | #define nullable
15 | #define _Nullable
16 | #define _Nonnull
17 | #endif
18 |
19 | #ifndef NS_ASSUME_NONNULL_BEGIN
20 | #define NS_ASSUME_NONNULL_BEGIN
21 | #endif
22 |
23 | #ifndef NS_ASSUME_NONNULL_END
24 | #define NS_ASSUME_NONNULL_END
25 | #endif
26 |
27 | #if __has_feature(objc_generics)
28 | #define CLS_GENERIC_NSARRAY(type) NSArray
29 | #define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
30 | #else
31 | #define CLS_GENERIC_NSARRAY(type) NSArray
32 | #define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
33 | #endif
34 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Headers/CLSLogging.h:
--------------------------------------------------------------------------------
1 | //
2 | // CLSLogging.h
3 | // Crashlytics
4 | //
5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
6 | //
7 | #ifdef __OBJC__
8 | #import "CLSAttributes.h"
9 | #import
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 | #endif
13 |
14 |
15 |
16 | /**
17 | *
18 | * The CLS_LOG macro provides as easy way to gather more information in your log messages that are
19 | * sent with your crash data. CLS_LOG prepends your custom log message with the function name and
20 | * line number where the macro was used. If your app was built with the DEBUG preprocessor macro
21 | * defined CLS_LOG uses the CLSNSLog function which forwards your log message to NSLog and CLSLog.
22 | * If the DEBUG preprocessor macro is not defined CLS_LOG uses CLSLog only.
23 | *
24 | * Example output:
25 | * -[AppDelegate login:] line 134 $ login start
26 | *
27 | * If you would like to change this macro, create a new header file, unset our define and then define
28 | * your own version. Make sure this new header file is imported after the Crashlytics header file.
29 | *
30 | * #undef CLS_LOG
31 | * #define CLS_LOG(__FORMAT__, ...) CLSNSLog...
32 | *
33 | **/
34 | #ifdef __OBJC__
35 | #ifdef DEBUG
36 | #define CLS_LOG(__FORMAT__, ...) CLSNSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
37 | #else
38 | #define CLS_LOG(__FORMAT__, ...) CLSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
39 | #endif
40 | #endif
41 |
42 | /**
43 | *
44 | * Add logging that will be sent with your crash data. This logging will not show up in the system.log
45 | * and will only be visible in your Crashlytics dashboard.
46 | *
47 | **/
48 |
49 | #ifdef __OBJC__
50 | OBJC_EXTERN void CLSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
51 | OBJC_EXTERN void CLSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0);
52 |
53 | /**
54 | *
55 | * Add logging that will be sent with your crash data. This logging will show up in the system.log
56 | * and your Crashlytics dashboard. It is not recommended for Release builds.
57 | *
58 | **/
59 | OBJC_EXTERN void CLSNSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
60 | OBJC_EXTERN void CLSNSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0);
61 |
62 |
63 | NS_ASSUME_NONNULL_END
64 | #endif
65 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Headers/CLSReport.h:
--------------------------------------------------------------------------------
1 | //
2 | // CLSReport.h
3 | // Crashlytics
4 | //
5 | // Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
6 | //
7 |
8 | #import
9 | #import "CLSAttributes.h"
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | /**
14 | * The CLSCrashReport protocol is deprecated. See the CLSReport class and the CrashyticsDelegate changes for details.
15 | **/
16 | @protocol CLSCrashReport
17 |
18 | @property (nonatomic, copy, readonly) NSString *identifier;
19 | @property (nonatomic, copy, readonly) NSDictionary *customKeys;
20 | @property (nonatomic, copy, readonly) NSString *bundleVersion;
21 | @property (nonatomic, copy, readonly) NSString *bundleShortVersionString;
22 | @property (nonatomic, copy, readonly) NSDate *crashedOnDate;
23 | @property (nonatomic, copy, readonly) NSString *OSVersion;
24 | @property (nonatomic, copy, readonly) NSString *OSBuildVersion;
25 |
26 | @end
27 |
28 | /**
29 | * The CLSReport exposes an interface to the phsyical report that Crashlytics has created. You can
30 | * use this class to get information about the event, and can also set some values after the
31 | * event has occured.
32 | **/
33 | @interface CLSReport : NSObject
34 |
35 | - (instancetype)init NS_UNAVAILABLE;
36 | + (instancetype)new NS_UNAVAILABLE;
37 |
38 | /**
39 | * Returns the session identifier for the report.
40 | **/
41 | @property (nonatomic, copy, readonly) NSString *identifier;
42 |
43 | /**
44 | * Returns the custom key value data for the report.
45 | **/
46 | @property (nonatomic, copy, readonly) NSDictionary *customKeys;
47 |
48 | /**
49 | * Returns the CFBundleVersion of the application that generated the report.
50 | **/
51 | @property (nonatomic, copy, readonly) NSString *bundleVersion;
52 |
53 | /**
54 | * Returns the CFBundleShortVersionString of the application that generated the report.
55 | **/
56 | @property (nonatomic, copy, readonly) NSString *bundleShortVersionString;
57 |
58 | /**
59 | * Returns the date that the report was created.
60 | **/
61 | @property (nonatomic, copy, readonly) NSDate *dateCreated;
62 |
63 | /**
64 | * Returns the os version that the application crashed on.
65 | **/
66 | @property (nonatomic, copy, readonly) NSString *OSVersion;
67 |
68 | /**
69 | * Returns the os build version that the application crashed on.
70 | **/
71 | @property (nonatomic, copy, readonly) NSString *OSBuildVersion;
72 |
73 | /**
74 | * Returns YES if the report contains any crash information, otherwise returns NO.
75 | **/
76 | @property (nonatomic, assign, readonly) BOOL isCrash;
77 |
78 | /**
79 | * You can use this method to set, after the event, additional custom keys. The rules
80 | * and semantics for this method are the same as those documented in Crashlytics.h. Be aware
81 | * that the maximum size and count of custom keys is still enforced, and you can overwrite keys
82 | * and/or cause excess keys to be deleted by using this method.
83 | **/
84 | - (void)setObjectValue:(nullable id)value forKey:(NSString *)key;
85 |
86 | /**
87 | * Record an application-specific user identifier. See Crashlytics.h for details.
88 | **/
89 | @property (nonatomic, copy, nullable) NSString * userIdentifier;
90 |
91 | /**
92 | * Record a user name. See Crashlytics.h for details.
93 | **/
94 | @property (nonatomic, copy, nullable) NSString * userName;
95 |
96 | /**
97 | * Record a user email. See Crashlytics.h for details.
98 | **/
99 | @property (nonatomic, copy, nullable) NSString * userEmail;
100 |
101 | @end
102 |
103 | NS_ASSUME_NONNULL_END
104 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Headers/CLSStackFrame.h:
--------------------------------------------------------------------------------
1 | //
2 | // CLSStackFrame.h
3 | // Crashlytics
4 | //
5 | // Copyright 2015 Crashlytics, Inc. All rights reserved.
6 | //
7 |
8 | #import
9 | #import "CLSAttributes.h"
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | /**
14 | *
15 | * This class is used in conjunction with -[Crashlytics recordCustomExceptionName:reason:frameArray:] to
16 | * record information about non-ObjC/C++ exceptions. All information included here will be displayed
17 | * in the Crashlytics UI, and can influence crash grouping. Be particularly careful with the use of the
18 | * address property. If set, Crashlytics will attempt symbolication and could overwrite other properities
19 | * in the process.
20 | *
21 | **/
22 | @interface CLSStackFrame : NSObject
23 |
24 | + (instancetype)stackFrame;
25 | + (instancetype)stackFrameWithAddress:(NSUInteger)address;
26 | + (instancetype)stackFrameWithSymbol:(NSString *)symbol;
27 |
28 | @property (nonatomic, copy, nullable) NSString *symbol;
29 | @property (nonatomic, copy, nullable) NSString *library;
30 | @property (nonatomic, copy, nullable) NSString *fileName;
31 | @property (nonatomic, assign) uint32_t lineNumber;
32 | @property (nonatomic, assign) uint64_t offset;
33 | @property (nonatomic, assign) uint64_t address;
34 |
35 | @end
36 |
37 | NS_ASSUME_NONNULL_END
38 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Modules/module.modulemap:
--------------------------------------------------------------------------------
1 | framework module Crashlytics {
2 | header "Crashlytics.h"
3 | header "Answers.h"
4 | header "ANSCompatibility.h"
5 | header "CLSLogging.h"
6 | header "CLSReport.h"
7 | header "CLSStackFrame.h"
8 | header "CLSAttributes.h"
9 |
10 | export *
11 |
12 | link "z"
13 | link "c++"
14 | }
15 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildMachineOSBuild
6 | 14F1021
7 | CFBundleDevelopmentRegion
8 | English
9 | CFBundleExecutable
10 | Crashlytics
11 | CFBundleIdentifier
12 | com.twitter.crashlytics.mac
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | Crashlytics
17 | CFBundlePackageType
18 | FMWK
19 | CFBundleShortVersionString
20 | 3.4.1
21 | CFBundleSignature
22 | ????
23 | CFBundleSupportedPlatforms
24 |
25 | MacOSX
26 |
27 | CFBundleVersion
28 | 92
29 | DTCompiler
30 | com.apple.compilers.llvm.clang.1_0
31 | DTPlatformBuild
32 | 7B1005
33 | DTPlatformVersion
34 | GM
35 | DTSDKBuild
36 | 15A278
37 | DTSDKName
38 | macosx10.11
39 | DTXcode
40 | 0711
41 | DTXcodeBuild
42 | 7B1005
43 | NSHumanReadableCopyright
44 | Copyright © 2015 Crashlytics, Inc. All rights reserved.
45 | UIDeviceFamily
46 |
47 | 1
48 | 2
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/A/_CodeSignature/CodeResources:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | files
6 |
7 | Resources/Info.plist
8 |
9 | 2WPfQTDP9K8ajX1IM/NFR47EgJs=
10 |
11 | Resources/en.lproj/InfoPlist.strings
12 |
13 | hash
14 |
15 | MiLKDDnrUKr4EmuvhS5VQwxHGK8=
16 |
17 | optional
18 |
19 |
20 |
21 | files2
22 |
23 | Headers/CLSLogging.h
24 |
25 | zcn6IoylvVofwfxrmJZrxDN7Tp0=
26 |
27 | Headers/CLSReport.h
28 |
29 | Tu7M2FRwYUeX+Yv7uFTV4jHKafA=
30 |
31 | Headers/CLSStackFrame.h
32 |
33 | 4XDKnrO7wRNttwP1TQ2Ed8CuRqQ=
34 |
35 | Headers/Crashlytics.h
36 |
37 | YfdjEaVRyq9Ds/1gYu4lnfQl8Ks=
38 |
39 | Modules/module.modulemap
40 |
41 | FjLJjH4TslH+1dyccMCHCkT4VpY=
42 |
43 | Modules/module.private.modulemap
44 |
45 | TW6wf6BihLkE0wMg0gzvdoYDEf8=
46 |
47 | PrivateHeaders/CLSAsyncOperation.h
48 |
49 | xaAW5bVUvvOOAm8WSM96i+6q4F8=
50 |
51 | PrivateHeaders/CLSAsyncOperation_Private.h
52 |
53 | IfcvxYVaMpOdRjsKhS7DLJUg/08=
54 |
55 | PrivateHeaders/Crashlytics_Errors.h
56 |
57 | g/pJdp+zfBGskCqr1rUZB/FMeHc=
58 |
59 | PrivateHeaders/Crashlytics_Platform.h
60 |
61 | uz96BQuMa25BzWR4fa39cFbAe60=
62 |
63 | PrivateHeaders/Crashlytics_WebKit.h
64 |
65 | nOQUR6IGXGnybgvIFS9xazi00Y8=
66 |
67 | Resources/Info.plist
68 |
69 | 2WPfQTDP9K8ajX1IM/NFR47EgJs=
70 |
71 | Resources/en.lproj/InfoPlist.strings
72 |
73 | hash
74 |
75 | MiLKDDnrUKr4EmuvhS5VQwxHGK8=
76 |
77 | optional
78 |
79 |
80 |
81 | rules
82 |
83 | ^Resources/
84 |
85 | ^Resources/.*\.lproj/
86 |
87 | optional
88 |
89 | weight
90 | 1000
91 |
92 | ^Resources/.*\.lproj/locversion.plist$
93 |
94 | omit
95 |
96 | weight
97 | 1100
98 |
99 | ^version.plist$
100 |
101 |
102 | rules2
103 |
104 | .*\.dSYM($|/)
105 |
106 | weight
107 | 11
108 |
109 | ^(.*/)?\.DS_Store$
110 |
111 | omit
112 |
113 | weight
114 | 2000
115 |
116 | ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
117 |
118 | nested
119 |
120 | weight
121 | 10
122 |
123 | ^.*
124 |
125 | ^Info\.plist$
126 |
127 | omit
128 |
129 | weight
130 | 20
131 |
132 | ^PkgInfo$
133 |
134 | omit
135 |
136 | weight
137 | 20
138 |
139 | ^Resources/
140 |
141 | weight
142 | 20
143 |
144 | ^Resources/.*\.lproj/
145 |
146 | optional
147 |
148 | weight
149 | 1000
150 |
151 | ^Resources/.*\.lproj/locversion.plist$
152 |
153 | omit
154 |
155 | weight
156 | 1100
157 |
158 | ^[^/]+$
159 |
160 | nested
161 |
162 | weight
163 | 10
164 |
165 | ^embedded\.provisionprofile$
166 |
167 | weight
168 | 20
169 |
170 | ^version\.plist$
171 |
172 | weight
173 | 20
174 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/Crashlytics.framework/Versions/Current:
--------------------------------------------------------------------------------
1 | A
--------------------------------------------------------------------------------
/Crashlytics.framework/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # run
4 | #
5 | # Copyright (c) 2015 Crashlytics. All rights reserved.
6 |
7 | # Figure out where we're being called from
8 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
9 |
10 | # Quote path in case of spaces or special chars
11 | DIR="\"${DIR}"
12 |
13 | PATH_SEP="/"
14 | VALIDATE_COMMAND="uploadDSYM\" $@ validate"
15 | UPLOAD_COMMAND="uploadDSYM\" $@"
16 |
17 | # Ensure params are as expected, run in sync mode to validate
18 | eval $DIR$PATH_SEP$VALIDATE_COMMAND
19 | return_code=$?
20 |
21 | if [[ $return_code != 0 ]]; then
22 | exit $return_code
23 | fi
24 |
25 | # Verification passed, upload dSYM in background to prevent Xcode from waiting
26 | # Note: Validation is performed again before upload.
27 | # Output can still be found in Console.app
28 | eval $DIR$PATH_SEP$UPLOAD_COMMAND > /dev/null 2>&1 &
29 |
--------------------------------------------------------------------------------
/Crashlytics.framework/submit:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Crashlytics.framework/submit
--------------------------------------------------------------------------------
/Crashlytics.framework/uploadDSYM:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Crashlytics.framework/uploadDSYM
--------------------------------------------------------------------------------
/Fabric.framework/Fabric:
--------------------------------------------------------------------------------
1 | Versions/Current/Fabric
--------------------------------------------------------------------------------
/Fabric.framework/Headers:
--------------------------------------------------------------------------------
1 | Versions/Current/Headers
--------------------------------------------------------------------------------
/Fabric.framework/Modules:
--------------------------------------------------------------------------------
1 | Versions/Current/Modules
--------------------------------------------------------------------------------
/Fabric.framework/Resources:
--------------------------------------------------------------------------------
1 | Versions/Current/Resources
--------------------------------------------------------------------------------
/Fabric.framework/Versions/A/Fabric:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Fabric.framework/Versions/A/Fabric
--------------------------------------------------------------------------------
/Fabric.framework/Versions/A/Headers/FABAttributes.h:
--------------------------------------------------------------------------------
1 | //
2 | // FABAttributes.h
3 | // Fabric
4 | //
5 | // Copyright (c) 2015 Twitter. All rights reserved.
6 | //
7 |
8 | #pragma once
9 |
10 | #define FAB_UNAVAILABLE(x) __attribute__((unavailable(x)))
11 |
12 | #if __has_feature(nullability)
13 | #define fab_nullable nullable
14 | #define fab_nonnull nonnull
15 | #define fab_null_unspecified null_unspecified
16 | #define fab_null_resettable null_resettable
17 | #define __fab_nullable __nullable
18 | #define __fab_nonnull __nonnull
19 | #define __fab_null_unspecified __null_unspecified
20 | #else
21 | #define fab_nullable
22 | #define fab_nonnull
23 | #define fab_null_unspecified
24 | #define fab_null_resettable
25 | #define __fab_nullable
26 | #define __fab_nonnull
27 | #define __fab_null_unspecified
28 | #endif
29 |
30 | #ifndef NS_ASSUME_NONNULL_BEGIN
31 | #define NS_ASSUME_NONNULL_BEGIN
32 | #endif
33 |
34 | #ifndef NS_ASSUME_NONNULL_END
35 | #define NS_ASSUME_NONNULL_END
36 | #endif
37 |
38 |
39 | /**
40 | * The following macros are defined here to provide
41 | * backwards compatability. If you are still using
42 | * them you should migrate to the new versions that
43 | * are defined above.
44 | */
45 | #define FAB_NONNULL __fab_nonnull
46 | #define FAB_NULLABLE __fab_nullable
47 | #define FAB_START_NONNULL NS_ASSUME_NONNULL_BEGIN
48 | #define FAB_END_NONNULL NS_ASSUME_NONNULL_END
49 |
--------------------------------------------------------------------------------
/Fabric.framework/Versions/A/Headers/Fabric.h:
--------------------------------------------------------------------------------
1 | //
2 | // Fabric.h
3 | //
4 | // Copyright (c) 2015 Twitter. All rights reserved.
5 | //
6 |
7 | #import
8 | #import "FABAttributes.h"
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | #if TARGET_OS_IPHONE
13 | #if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
14 | #error "Fabric's minimum iOS version is 6.0"
15 | #endif
16 | #else
17 | #if __MAC_OS_X_VERSION_MIN_REQUIRED < 1070
18 | #error "Fabric's minimum OS X version is 10.7"
19 | #endif
20 | #endif
21 |
22 | /**
23 | * Fabric Base. Coordinates configuration and starts all provided kits.
24 | */
25 | @interface Fabric : NSObject
26 |
27 | /**
28 | * Initialize Fabric and all provided kits. Call this method within your App Delegate's `application:didFinishLaunchingWithOptions:` and provide the kits you wish to use.
29 | *
30 | * For example, in Objective-C:
31 | *
32 | * `[Fabric with:@[[Crashlytics class], [Twitter class], [Digits class], [MoPub class]]];`
33 | *
34 | * Swift:
35 | *
36 | * `Fabric.with([Crashlytics.self(), Twitter.self(), Digits.self(), MoPub.self()])`
37 | *
38 | * Only the first call to this method is honored. Subsequent calls are no-ops.
39 | *
40 | * @param kitClasses An array of kit Class objects
41 | *
42 | * @return Returns the shared Fabric instance. In most cases this can be ignored.
43 | */
44 | + (instancetype)with:(NSArray *)kitClasses;
45 |
46 | /**
47 | * Returns the Fabric singleton object.
48 | */
49 | + (instancetype)sharedSDK;
50 |
51 | /**
52 | * This BOOL enables or disables debug logging, such as kit version information. The default value is NO.
53 | */
54 | @property (nonatomic, assign) BOOL debug;
55 |
56 | /**
57 | * Unavailable. Use `+sharedSDK` to retrieve the shared Fabric instance.
58 | */
59 | - (id)init FAB_UNAVAILABLE("Use +sharedSDK to retrieve the shared Fabric instance.");
60 |
61 | @end
62 |
63 | NS_ASSUME_NONNULL_END
64 |
65 |
--------------------------------------------------------------------------------
/Fabric.framework/Versions/A/Modules/module.modulemap:
--------------------------------------------------------------------------------
1 | framework module Fabric {
2 | umbrella header "Fabric.h"
3 |
4 | export *
5 | module * { export * }
6 | }
--------------------------------------------------------------------------------
/Fabric.framework/Versions/A/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildMachineOSBuild
6 | 14F1021
7 | CFBundleDevelopmentRegion
8 | en
9 | CFBundleExecutable
10 | Fabric
11 | CFBundleIdentifier
12 | io.fabric.sdk.mac
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | Fabric
17 | CFBundlePackageType
18 | FMWK
19 | CFBundleShortVersionString
20 | 1.6.1
21 | CFBundleSignature
22 | ????
23 | CFBundleSupportedPlatforms
24 |
25 | MacOSX
26 |
27 | CFBundleVersion
28 | 37
29 | DTCompiler
30 | com.apple.compilers.llvm.clang.1_0
31 | DTPlatformBuild
32 | 7B91b
33 | DTPlatformVersion
34 | GM
35 | DTSDKBuild
36 | 15A278
37 | DTSDKName
38 | macosx10.11
39 | DTXcode
40 | 0710
41 | DTXcodeBuild
42 | 7B91b
43 | NSHumanReadableCopyright
44 | Copyright © 2015 Twitter. All rights reserved.
45 | UIDeviceFamily
46 |
47 | 1
48 | 2
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/Fabric.framework/Versions/Current:
--------------------------------------------------------------------------------
1 | A
--------------------------------------------------------------------------------
/Fabric.framework/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # run
4 | #
5 | # Copyright (c) 2015 Crashlytics. All rights reserved.
6 |
7 | # Figure out where we're being called from
8 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
9 |
10 | # Quote path in case of spaces or special chars
11 | DIR="\"${DIR}"
12 |
13 | PATH_SEP="/"
14 | VALIDATE_COMMAND="uploadDSYM\" $@ validate"
15 | UPLOAD_COMMAND="uploadDSYM\" $@"
16 |
17 | # Ensure params are as expected, run in sync mode to validate
18 | eval $DIR$PATH_SEP$VALIDATE_COMMAND
19 | return_code=$?
20 |
21 | if [[ $return_code != 0 ]]; then
22 | exit $return_code
23 | fi
24 |
25 | # Verification passed, upload dSYM in background to prevent Xcode from waiting
26 | # Note: Validation is performed again before upload.
27 | # Output can still be found in Console.app
28 | eval $DIR$PATH_SEP$UPLOAD_COMMAND > /dev/null 2>&1 &
29 |
--------------------------------------------------------------------------------
/Fabric.framework/uploadDSYM:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Fabric.framework/uploadDSYM
--------------------------------------------------------------------------------
/Playground.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Playground - noun: a place where people can play
2 |
3 | import Cocoa
4 |
5 | var str = "Hello, playground"
6 |
--------------------------------------------------------------------------------
/Playground.playground/Sources/SupportCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This file (and all other Swift source files in the Sources directory of this playground) will be precompiled into a framework which is automatically made available to Playground.playground.
3 | //
4 |
--------------------------------------------------------------------------------
/Playground.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Playground.playground/timeline.xctimeline:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 |
3 | link_with 'App Reviews', 'App ReviewsTests'
4 |
5 | platform :osx, '10.10'
6 |
7 | use_frameworks!
8 |
9 | pod 'Sparkle', '1.13.0'
10 | pod 'SwiftyJSON', '2.3.2'
11 | pod 'Alamofire', '3.1.3'
12 | pod 'EDStarRating', '1.1'
13 | pod 'SimpleCocoaAnalytics', '~> 0.1'
14 | pod 'Ensembles', '1.4.3'
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Alamofire (3.1.3)
3 | - EDStarRating (1.1)
4 | - Ensembles (1.4.3):
5 | - Ensembles/Core (= 1.4.3)
6 | - Ensembles/Core (1.4.3)
7 | - SimpleCocoaAnalytics (0.1.0)
8 | - Sparkle (1.13.0)
9 | - SwiftyJSON (2.3.2)
10 |
11 | DEPENDENCIES:
12 | - Alamofire (= 3.1.3)
13 | - EDStarRating (= 1.1)
14 | - Ensembles (= 1.4.3)
15 | - SimpleCocoaAnalytics (~> 0.1)
16 | - Sparkle (= 1.13.0)
17 | - SwiftyJSON (= 2.3.2)
18 |
19 | SPEC CHECKSUMS:
20 | Alamofire: 9f93b56389e48def9220dd57d1f44b1927229a5a
21 | EDStarRating: a41a6440a945020745d0e9ff8ada7d1afa952114
22 | Ensembles: dec7c46616bbb1fb5611f19567b0e0725edab6af
23 | SimpleCocoaAnalytics: cc95e551884ec851002c668d15b71b8a73e92f56
24 | Sparkle: d04d17a32eaddab19471d0c3d9db15164afdbc6e
25 | SwiftyJSON: 04ccea08915aa0109039157c7974cf0298da292a
26 |
27 | COCOAPODS: 0.39.0.beta.5
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # App Reviews
2 | [](https://travis-ci.org/knutigro/AppReviews)
3 | 
4 | [](https://github.com/knutigro/AppReviews/blob/develop/LICENSE)
5 |
6 | [](http://knutigro.github.io/apps/app-reviews/)
7 | App Reviews for Mac is an app that makes it super simple for Mac OS X users to keep track of user reviews for iPhone apps. App Reviews runs in the statusbar and notifies you when new reviews come in.
8 |
9 | Please have a look at the [App Reviews website](http://knutigro.github.io/apps/app-reviews/) for link to latest signed binary and more info about the app.
10 |
11 | [](https://flattr.com/submit/auto?user_id=knutigro&url=https://github.com/knutigro/app-reviews-osx&title=AppReviews&language=Swift&tags=github&category=software)
12 |
13 | ## Screenshots
14 |
15 | 
16 |
17 | ## Author
18 |
19 | Knut Inge Grosland, ”hei@knutinge.com”
20 |
21 | ## License
22 |
23 | App Reviews is available under the GNU General Public License v3.0 license. See the [LICENSE](LICENSE) file for more info.
24 |
25 |
--------------------------------------------------------------------------------
/ReviewManager/ApplicationUpdater.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewUpdater.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-18.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 | import SwiftyJSON
11 |
12 | let kTimerInterval = 60.0 // 60.0 * 60 // Update interval in seconds -> Each Hour
13 | let kDefaultReviewUpdateInterval = 60.0 * 60 // Update interval in seconds -> Each Hour
14 |
15 | class ApplicationUpdater {
16 |
17 | private var timer: Timer?
18 | private var applications = [Application]()
19 |
20 | var numberOfMonitoredApplications: Int {
21 | return applications.count
22 | }
23 |
24 | // MARK: - Init & teardown
25 |
26 | init() {
27 |
28 | let _ = NSNotificationCenter.defaultCenter().addObserverForName(kDidUpdateApplicationNotification, object: nil, queue: nil) { [weak self] notification in
29 | self?.updateMonitoredApplications()
30 | }
31 |
32 | let _ = NSNotificationCenter.defaultCenter().addObserverForName(kDidUpdateApplicationSettingsNotification, object: nil, queue: nil) { [weak self] notification in
33 | self?.updateMonitoredApplications()
34 | }
35 |
36 | let _ = NSNotificationCenter.defaultCenter().addObserverForName(kDidUpdateReviewsNotification, object: nil, queue: nil) { [weak self] notification in
37 | self?.updateMonitoredApplications()
38 | }
39 |
40 | timer = Timer.repeatEvery(kTimerInterval) { [weak self] inTimer in
41 | self?.updateReviewsForAllApplications()
42 | }
43 |
44 | updateMonitoredApplications();
45 | }
46 |
47 | private func updateReviewsForAllApplications() {
48 | dispatch_async(dispatch_get_main_queue(), { [weak self] () -> Void in
49 | guard let strongSelf = self else { return }
50 | for application in strongSelf.applications {
51 | if application.settings.shouldUpdateReviews {
52 | strongSelf.fetchReviewsForApplication(application.objectID)
53 | }
54 | }
55 | })
56 | }
57 |
58 | private func updateMonitoredApplications() {
59 |
60 | dispatch_async(dispatch_get_main_queue(), { [weak self] () -> Void in
61 | guard let strongSelf = self else { return }
62 | if let dBApplications = DatabaseHandler.allApplications(ReviewManager.managedObjectContext()) {
63 | for dBApplication in dBApplications {
64 | if !strongSelf.applications.contains(dBApplication) {
65 | strongSelf.applications.append(dBApplication)
66 | strongSelf.fetchReviewsForApplication(dBApplication.objectID)
67 | }
68 | }
69 |
70 | var applicationsToRemove = [Application]()
71 | for application in strongSelf.applications {
72 | if !dBApplications.contains(application) {
73 | applicationsToRemove.append(application)
74 | }
75 | }
76 |
77 | for applicationToRemove in applicationsToRemove {
78 | strongSelf.applications.removeObject(applicationToRemove)
79 | }
80 | }
81 | })
82 |
83 | }
84 |
85 | // MARK: - Reviews handling
86 |
87 | func fetchReviewsForApplication(objectId: NSManagedObjectID) {
88 | do {
89 | if let fetchApplication = try ReviewManager.managedObjectContext().existingObjectWithID(objectId) as? Application {
90 | let itunesService = ItunesService(apId: fetchApplication.trackId, storeId: nil)
91 | itunesService.fetchReviews(itunesService.url) {
92 | (reviews: [JSON], error: NSError?) in
93 | DatabaseHandler.saveReviews(reviews, applactionObjectId: fetchApplication.objectID)
94 | }
95 | }
96 | } catch let error as NSError {
97 | print(error)
98 | } catch {
99 | fatalError()
100 | }
101 | }
102 |
103 | func resetNewReviewsCountForApplication(objectId: NSManagedObjectID) {
104 | DatabaseHandler.resetNewReviewsCountForApplication(objectId)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/ReviewManager/Categories/Application+String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Application+String.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-11.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: String Output
12 |
13 | extension Application {
14 |
15 | func toShortString() -> String {
16 | var string = trackName
17 | string += "\n" + sellerName
18 |
19 | return string
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/ReviewManager/Categories/Array+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+Utils.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-19.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | extension Array {
10 | mutating func removeObject(object: U) {
11 | var index: Int?
12 | for (idx, objectToCompare) in self.enumerate() {
13 | if let to = objectToCompare as? U {
14 | if object == to {
15 | index = idx
16 | }
17 | }
18 | }
19 |
20 | if(index != nil) {
21 | removeAtIndex(index!)
22 | }
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/ReviewManager/Categories/Review+String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Application+String.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-10.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: String Output
12 |
13 | extension Review {
14 |
15 | func toString() -> String {
16 | var string = self.rating.integerValue.toEmojiStars()
17 | string += "\n" + title
18 | string += "\n" + content
19 | string += "\n" + author
20 | string += "\n" + uri
21 | string += "\n" + application.trackName + " (" + version + ")"
22 |
23 | return string
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/ReviewManager/Categories/String+AppVersion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Application+Version.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-19.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // AppVersion
12 |
13 | extension NSString {
14 |
15 | private func versionAsIntegerArray() -> [Int] {
16 |
17 | let versionComponents = (componentsSeparatedByString("."))
18 | var versionComponentsAsIntegers = [Int]()
19 |
20 | for component in versionComponents {
21 |
22 | let range = Range(start: component.startIndex, end: component.endIndex)
23 |
24 | // NSRegularExpressionSearch
25 | let componentString = component.stringByReplacingOccurrencesOfString("[^0-9]", withString: "", options: NSStringCompareOptions(rawValue: 1024), range: range)
26 | if let intComponent = Int(componentString) {
27 | versionComponentsAsIntegers.append(intComponent)
28 | }
29 | }
30 |
31 | return versionComponentsAsIntegers
32 | }
33 |
34 | func compareVersion(version: NSString) -> NSComparisonResult {
35 |
36 | let myIntegerArray = versionAsIntegerArray()
37 | let applicationArray = version.versionAsIntegerArray()
38 |
39 | for (var i = 0; i < myIntegerArray.count; i++) {
40 | let myVersion = myIntegerArray[i]
41 | let applicationVersion = i < applicationArray.count ? applicationArray[i]: 0
42 | if myVersion > applicationVersion {
43 | return .OrderedDescending
44 | } else if myVersion < applicationVersion {
45 | return .OrderedAscending
46 | }
47 | }
48 |
49 | return .OrderedSame
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/ReviewManager/Categories/String+Emoji.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Emoji.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-12.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | // Emoji extension
10 |
11 | extension Int {
12 |
13 | func toEmojiStars() -> String {
14 | var starArray = [String]()
15 | for _ in 1 ... self {
16 | starArray.append("⭐️")
17 | }
18 |
19 | return starArray.joinWithSeparator("")
20 | }
21 |
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/ReviewManager/DatabaseHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // COImportController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-09.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AppKit
11 | import SwiftyJSON
12 |
13 | class DatabaseHandler {
14 |
15 | typealias CompletionBlock = () -> ()
16 |
17 | // MARK: - Applications handling
18 |
19 | class func saveApplication(applicationJSON: JSON) {
20 | DatabaseHandler.saveDataInContext({ (context) -> Void in
21 | if applicationJSON.isApplicationEntity, let apID = applicationJSON.trackId {
22 | if let application = Application.getWithAppId(apID, context: context) {
23 | application.updatedAt = NSDate()
24 | application.updateWithJSON(applicationJSON)
25 | } else {
26 | let application = Application.new(apID, context: context)
27 | application.updateWithJSON(applicationJSON)
28 | }
29 | }
30 | })
31 | }
32 |
33 | class func removeApplication(objectId: NSManagedObjectID) {
34 | DatabaseHandler.saveDataInContext({ (context) -> Void in
35 | do {
36 | let application = try context.existingObjectWithID(objectId)
37 | context.deleteObject(application)
38 | } catch let error as NSError {
39 | print(error)
40 | } catch {
41 | fatalError()
42 | }
43 | })
44 | }
45 |
46 | class func resetNewReviewsCountForApplication(objectId: NSManagedObjectID) {
47 | DatabaseHandler.saveDataInContext({ (context) -> Void in
48 | do {
49 | if let application = try context.existingObjectWithID(objectId) as? Application {
50 | application.settings.resetNewReviews()
51 | }
52 | } catch let error as NSError {
53 | print(error)
54 | } catch {
55 | fatalError()
56 | }
57 | })
58 | }
59 |
60 | class func allApplications(context: NSManagedObjectContext) -> [Application]? {
61 |
62 | let fetchRequest = NSFetchRequest(entityName: kEntityNameApplication)
63 | var result: [AnyObject]?
64 | do {
65 | result = try context.executeFetchRequest(fetchRequest)
66 | } catch let error as NSError {
67 | print(error)
68 | }
69 |
70 | return result as? [Application]
71 | }
72 |
73 | class func numberOfReviewsForApplication(objectId: NSManagedObjectID, rating: Int?, context: NSManagedObjectContext) -> (one: Int, two: Int, three: Int, four: Int, five: Int) {
74 | var error: NSError?
75 | var one = 0, two = 0, three = 0, four = 0, five = 0
76 | do {
77 | if let application = try context.existingObjectWithID(objectId) as? Application {
78 | let fetchRequest = NSFetchRequest(entityName: kEntityNameReview)
79 |
80 | fetchRequest.predicate = NSPredicate(format: "application = %@ AND rating == 1", application)
81 | one = context.countForFetchRequest(fetchRequest, error: &error)
82 |
83 | fetchRequest.predicate = NSPredicate(format: "application = %@ AND rating == 2", application)
84 | two = context.countForFetchRequest(fetchRequest, error: &error)
85 |
86 | fetchRequest.predicate = NSPredicate(format: "application = %@ AND rating == 3", application)
87 | three = context.countForFetchRequest(fetchRequest, error: &error)
88 |
89 | fetchRequest.predicate = NSPredicate(format: "application = %@ AND rating == 4", application)
90 | four = context.countForFetchRequest(fetchRequest, error: &error)
91 |
92 | fetchRequest.predicate = NSPredicate(format: "application = %@ AND rating == 5", application)
93 | five = context.countForFetchRequest(fetchRequest, error: &error)
94 | }
95 | } catch let error1 as NSError {
96 | error = error1
97 | } catch {
98 | fatalError()
99 | }
100 |
101 | if error != nil { print(error) }
102 |
103 | return (one, two, three, four, five)
104 | }
105 |
106 | class func saveReviews(reviews: [JSON], applactionObjectId objectId: NSManagedObjectID) {
107 | if reviews.count == 0 { return }
108 |
109 | DatabaseHandler.saveDataInContext({ (context) -> Void in
110 | do {
111 | if let application = try context.existingObjectWithID(objectId) as? Application {
112 |
113 | var updatedReviews = [Review]()
114 |
115 | for var index = 0; index < reviews.count; index++ {
116 | let entry = reviews[index]
117 |
118 | if entry.isReviewEntity, let apID = entry.reviewApID {
119 | var review: Review!
120 |
121 | if let newReview = Review.get(apID, context: context) {
122 | // Review allready exist in database
123 | review = newReview
124 | } else {
125 | // create new review
126 | review = Review.new(apID, context: context)
127 | application.settings.increaseNewReviews()
128 | }
129 |
130 | review.updateWithJSON(entry)
131 | review.country = ""
132 | review.updatedAt = NSDate()
133 | let reviews = application.mutableSetValueForKey("reviews")
134 | reviews.addObject(review)
135 | review.application = application
136 | updatedReviews.append(review)
137 | }
138 | }
139 | application.settings.updatedAt = NSDate()
140 | application.settings.reviewsUpdatedAt = NSDate()
141 | application.settings.nextUpdateAt = NSDate().dateByAddingTimeInterval(kDefaultReviewUpdateInterval)
142 | }
143 | } catch let error as NSError {
144 | print(error)
145 | } catch {
146 | fatalError()
147 | }
148 | })
149 | }
150 |
151 | // MARK: - DB Handling
152 |
153 | class func saveDataInContext(saveBlock: (context: NSManagedObjectContext) -> Void) {
154 | DatabaseHandler.saveDataInContext(saveBlock, completion: nil)
155 | }
156 |
157 | class func saveDataInContext(saveBlock: (context: NSManagedObjectContext) -> Void, completion: CompletionBlock?) {
158 |
159 | let context = ReviewManager.backgroundObjectContext()
160 | context.performBlock { () -> Void in
161 | saveBlock(context: context)
162 |
163 | if context.hasChanges {
164 | do {
165 | try context.save()
166 | } catch let error as NSError {
167 | print(error)
168 | } catch {
169 | fatalError()
170 | }
171 | }
172 |
173 | if let completion = completion {
174 | dispatch_async(dispatch_get_main_queue(), { () -> Void in
175 | completion()
176 | })
177 | }
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/ReviewManager/ItunesService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // COReviewFetcher.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-09.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Alamofire
11 | import SwiftyJSON
12 |
13 |
14 |
15 | class ItunesService {
16 |
17 | var url: String {
18 | return urlHandler.nextUrl ?? urlHandler.initialUrl
19 | }
20 |
21 | let apId: String
22 | let storeId: String?
23 | var updated: NSDate?
24 | let urlHandler: ItunesUrlHandler
25 |
26 | // MARK: - Init & teardown
27 |
28 | init(apId: String, storeId: String?) {
29 | self.apId = apId
30 | self.storeId = storeId
31 | urlHandler = ItunesUrlHandler(apId: apId, storeId: storeId)
32 | }
33 |
34 | // MARK: - Update object
35 |
36 | func updateWithJSON(json: JSON) {
37 |
38 | if let dateString = json.itunesReviewsUpdatedAt {
39 | let dateFormatter = NSDateFormatter()
40 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss-SS:SS'"
41 | if let date = dateFormatter.dateFromString(dateString) {
42 | updated = date
43 | }
44 | }
45 |
46 | urlHandler.updateWithJSON(json.itunesFeedLinks)
47 | }
48 |
49 | // MARK: - Fetching
50 |
51 | func fetchReviews(url: String, completion: (reviews: [JSON], error: NSError?) -> Void) {
52 |
53 | Alamofire.request(.GET, url, parameters: nil)
54 | .responseJSON { response in
55 |
56 | if response.result.error != nil {
57 | NSLog("Error: \(response.result.error)")
58 | print(response.request)
59 | print(response)
60 | completion(reviews: [], error: response.result.error)
61 | } else {
62 | let json = JSON(response.result.value!)
63 | let reviews = json.itunesReviews
64 |
65 | completion(reviews: reviews, error: nil)
66 |
67 | // TODO: THIS WILL ALLWAYS FAIL SINCE nexturl is nil from the first round
68 | if let nextUrl = self.urlHandler.nextUrl {
69 | if reviews.count > 0 {
70 | self.updateWithJSON(json)
71 | self.fetchReviews(nextUrl, completion: completion)
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 | class func fetchApplications(name: String, completion: (success: Bool, applications: JSON?, error: NSError?) -> Void) {
79 |
80 | let url = "https://itunes.apple.com/search"
81 | let params = ["term": name, "entity": "software"]
82 |
83 | Alamofire.request(.GET, url, parameters: params)
84 | .responseJSON { response in
85 |
86 | if(response.result.error != nil) {
87 | NSLog("Error: \(response.result.error)")
88 | print(response.request)
89 | print(response)
90 | completion(success: false, applications: nil, error: response.result.error)
91 | } else {
92 | var json = JSON(response.result.value!)
93 | completion(success: true, applications: json["results"], error: nil)
94 | }
95 | }
96 | }
97 | }
98 |
99 | // MARK: Extension for reviewFeed
100 |
101 | extension JSON {
102 | var itunesFeed: JSON { return self["feed"] }
103 | var itunesReviews: [JSON] { return self.itunesFeed["entry"].arrayValue }
104 | var itunesReviewsUpdatedAt: String? { return self.itunesFeed["updated"]["label"].string }
105 | var itunesFeedLinks: [JSON] { return self.itunesFeed["link"].arrayValue }
106 | }
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/ReviewManager/ItunesUrlHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItunesUrlHandler.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-05-10.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftyJSON
11 |
12 | class ItunesPage {
13 |
14 | var page: Int
15 | var url: String
16 | var nextUrl: String?
17 |
18 | init(url: String, page: Int) {
19 | self.url = url;
20 | self.page = page;
21 | }
22 |
23 | convenience init(url: String, page: Int, json: [JSON]) {
24 | self.init(url: url, page: page)
25 | for jsonLink in json {
26 | let attributes = jsonLink["attributes"]
27 |
28 | if attributes["rel"].stringValue == "next" {
29 | nextUrl = attributes["href"].string?.stringByRemovingItunesFormatting()
30 | }
31 |
32 | // We dont get a valid next url , try to create one from the previous
33 | if nextUrl == nil && url.containPage() {
34 | if let nextUrl = ItunesPage.urlByIncreasingPage(url, page: page) {
35 | self.nextUrl = nextUrl.url
36 | }
37 | }
38 | }
39 | }
40 |
41 | func isEqualPage(page: ItunesPage) -> Bool {
42 | return self.page == page.page
43 | }
44 |
45 | class func urlByIncreasingPage(urlString: String?, page: Int?) -> (url: String, page: Int)? {
46 | if let urlstring = urlString, let page = page {
47 | if let _ = NSURL(string: urlstring) {
48 | let old = String(format: "page=%i", page)
49 | let next = String(format: "page=%i", page + 1)
50 | return (urlstring.stringByReplacingOccurrencesOfString(old, withString: next), page + 1)
51 | }
52 | }
53 |
54 | return nil
55 | }
56 | }
57 |
58 | class ItunesUrlHandler {
59 |
60 | private var storeId: String?
61 | private var apId: String
62 |
63 | var nextUrl: String? {
64 | return pages.last?.nextUrl
65 | }
66 |
67 | var initialUrl: String {
68 | let storePath = storeId != nil ? ("/" + self.storeId!): ""
69 | return "https://itunes.apple.com" + storePath + "/rss/customerreviews/id=" + self.apId + "/json"
70 | }
71 |
72 | var pages = [ItunesPage]()
73 |
74 | init(apId: String, storeId: String?) {
75 | self.apId = apId
76 | self.storeId = storeId
77 | pages.append(ItunesPage(url: initialUrl, page: 0))
78 | }
79 |
80 | func updateWithJSON(json: [JSON]) {
81 | if let previousPage = pages.last {
82 | let newPage = ItunesPage(url: previousPage.url, page: previousPage.page + 1, json: json)
83 | if isNewPage(newPage) {
84 | pages.append(newPage)
85 | }
86 | }
87 | }
88 |
89 | func isNewPage(newPage: ItunesPage) -> Bool {
90 | for page in pages {
91 | if page.isEqualPage(newPage) {
92 | return false;
93 | }
94 | }
95 | return true
96 | }
97 | }
98 |
99 | // MARK: ItunesUrlHandler
100 |
101 | extension String {
102 | func stringByRemovingDoubleSlashes() -> String {
103 | return stringByReplacingOccurrencesOfString("\\/", withString: "/")
104 | }
105 |
106 | func stringByRemovingItunesFormatting() -> String {
107 | let temp = stringByRemovingDoubleSlashes()
108 | if let url = NSURL(string: temp) {
109 | if let pathComponents = url.pathComponents {
110 | var newUrlString = ""
111 | var newPathSet = Set()
112 |
113 | for path in pathComponents {
114 | let isXML = path == "xml?urlDesc="
115 |
116 | if !newPathSet.contains(path) && !isXML {
117 | if newPathSet.isEmpty {
118 | newUrlString = newUrlString.stringByAppendingString(path)
119 | } else if newPathSet.count == 1 {
120 | newUrlString = newUrlString.stringByAppendingFormat("//%@", path)
121 | } else {
122 | newUrlString = newUrlString.stringByAppendingFormat("/%@", path)
123 | }
124 | newPathSet.insert(path)
125 | }
126 | }
127 | return newUrlString
128 | }
129 | }
130 | return temp
131 | }
132 |
133 | func containPage() -> Bool {
134 | return page() != nil;
135 | }
136 |
137 | func page() -> Int? {
138 | if let url = NSURL(string: self) {
139 | if let pathComponents = url.pathComponents {
140 | for path in pathComponents {
141 | if path.rangeOfString("page=") != nil {
142 | let page = path.stringByReplacingOccurrencesOfString("page=", withString: "")
143 | return Int(page)
144 | }
145 | }
146 | }
147 | }
148 | return nil
149 | }
150 |
151 | }
152 |
--------------------------------------------------------------------------------
/ReviewManager/Models/AppReviews.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | AppReviews.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ReviewManager/Models/AppReviews.xcdatamodeld/AppReviews.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/ReviewManager/Models/Application.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Application.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-10.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftyJSON
11 |
12 | let kEntityNameApplication = "Application"
13 |
14 | @objc(Application)
15 |
16 | class Application: NSManagedObject {
17 |
18 | @NSManaged var artworkUrl60: String
19 | @NSManaged var artworkUrl512: String
20 | @NSManaged var artistViewUrl: String
21 | @NSManaged var fileSizeBytes: String
22 | @NSManaged var sellerUrl: String
23 | @NSManaged var averageUserRatingForCurrentVersion: NSNumber
24 | @NSManaged var userRatingCountForCurrentVersion: NSNumber
25 | @NSManaged var trackViewUrl: String
26 | @NSManaged var version: String
27 | @NSManaged var releaseDate: NSDate?
28 | @NSManaged var sellerName: String
29 | @NSManaged var artistId: String
30 | @NSManaged var artistName: String
31 | @NSManaged var itunesDescription: String
32 | @NSManaged var bundleId: String
33 | @NSManaged var trackId: String
34 | @NSManaged var trackName: String
35 | @NSManaged var primaryGenreName: String
36 | @NSManaged var primaryGenreId: String
37 | @NSManaged var releaseNotes: String
38 | @NSManaged var minimumOsVersion: String
39 | @NSManaged var averageUserRating: NSNumber
40 | @NSManaged var userRatingCount: NSNumber
41 | @NSManaged var createdAt: NSDate
42 | @NSManaged var updatedAt: NSDate
43 | @NSManaged var reviews: NSSet
44 | @NSManaged var settings: ApplicationSettings
45 |
46 | var fileSizeMb: Float {
47 | get {
48 | let fileSize = Int(fileSizeBytes) ?? 0
49 | let mb = Float(fileSize) / 1000000
50 | return max(mb, 0.0)
51 | }
52 | }
53 |
54 | // MARK: - Init & teardown
55 |
56 | override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
57 | super.init(entity: entity, insertIntoManagedObjectContext: context)
58 | }
59 |
60 | convenience init(insertIntoManagedObjectContext context: NSManagedObjectContext) {
61 | let entityDescription = NSEntityDescription.entityForName(kEntityNameApplication, inManagedObjectContext: context)
62 | self.init(entity: entityDescription!, insertIntoManagedObjectContext: context)
63 | }
64 |
65 | // MARK: - Class functions for create and insert and search
66 |
67 | class func getWithIds(ids: Set, context: NSManagedObjectContext) -> [Application]? {
68 | let fetchRequest = NSFetchRequest(entityName: kEntityNameApplication)
69 | fetchRequest.predicate = NSPredicate(format: "self in %@", Array(ids))
70 | var result: [AnyObject]?
71 | do {
72 | result = try context.executeFetchRequest(fetchRequest)
73 | } catch let error as NSError {
74 | print(error)
75 | }
76 |
77 | return result as? [Application]
78 | }
79 |
80 | class func getWithAppId(identifier: String, context: NSManagedObjectContext) -> Application? {
81 |
82 | let fetchRequest = NSFetchRequest(entityName: kEntityNameApplication)
83 | fetchRequest.predicate = NSPredicate(format: "trackId = %@", identifier)
84 |
85 | var result: [AnyObject]?
86 | do {
87 | result = try context.executeFetchRequest(fetchRequest)
88 | } catch let error as NSError {
89 | print(error)
90 | }
91 |
92 | return result?.last as? Application
93 | }
94 |
95 | class func new(identifier: String, context: NSManagedObjectContext) -> Application {
96 | let application = Application(insertIntoManagedObjectContext: context)
97 | application.trackId = identifier;
98 | application.settings = ApplicationSettings.new(application, context: context)
99 | application.createdAt = NSDate()
100 | application.updatedAt = NSDate()
101 |
102 | return application
103 | }
104 |
105 | class func getOrCreateNew(identifier: String, context: NSManagedObjectContext) -> Application {
106 | if let application = Application.getWithAppId(identifier, context: context) {
107 | return application
108 | } else {
109 | return Application.new(identifier, context: context)
110 | }
111 | }
112 |
113 | class func insertNewObjectIntoContext(context: NSManagedObjectContext) -> Application {
114 | return NSEntityDescription.insertNewObjectForEntityForName(kEntityNameApplication, inManagedObjectContext: context) as! Application
115 | }
116 | }
117 |
118 | // MARK: - Application extension of JSON
119 |
120 | extension Application {
121 |
122 | func updateWithJSON(json: JSON) {
123 |
124 | artworkUrl60 = json.artworkUrl60 ?? ""
125 | artworkUrl512 = json.artworkUrl512 ?? ""
126 | artistViewUrl = json.artistViewUrl ?? ""
127 | fileSizeBytes = json.fileSizeBytes ?? ""
128 | sellerUrl = json.sellerUrl ?? ""
129 | version = json.version ?? ""
130 | averageUserRatingForCurrentVersion = json.averageUserRatingForCurrentVersion
131 | userRatingCountForCurrentVersion = json.userRatingCountForCurrentVersion
132 | trackViewUrl = json.trackViewUrl ?? ""
133 | version = json.version ?? ""
134 | releaseDate = json.releaseDate
135 | sellerName = json.sellerName ?? ""
136 | artistId = json.artistId ?? ""
137 | artistName = json.artistName ?? ""
138 | itunesDescription = json.itunesDescription ?? ""
139 | bundleId = json.bundleId ?? ""
140 | trackId = json.trackId ?? ""
141 | trackName = json.trackName ?? ""
142 | primaryGenreName = json.primaryGenreName ?? ""
143 | primaryGenreId = json.primaryGenreId ?? ""
144 | releaseNotes = json.releaseNotes ?? ""
145 | minimumOsVersion = json.minimumOsVersion ?? ""
146 | averageUserRating = json.averageUserRating
147 | userRatingCount = json.userRatingCount
148 |
149 | }
150 | }
151 |
152 | // MARK: - JSON extension of Application
153 |
154 | extension JSON {
155 |
156 | var artworkUrl60: String? { return self["artworkUrl60"].string }
157 | var artworkUrl512: String? { return self["artworkUrl512"].string}
158 | var artistViewUrl: String? { return self["artistViewUrl"].string }
159 | var fileSizeBytes: String? { return self["fileSizeBytes"].string}
160 | var sellerUrl: String? { return self["sellerUrl"].string }
161 | var averageUserRatingForCurrentVersion: NSNumber { return NSNumber(float:(self["averageUserRatingForCurrentVersion"].stringValue as NSString).floatValue) }
162 | var userRatingCountForCurrentVersion: NSNumber { return NSNumber(integer: Int(self["userRatingCountForCurrentVersion"].stringValue) ?? 0) }
163 | var trackViewUrl: String? { return self["trackViewUrl"].string }
164 | var version: String? { return self["version"].string }
165 | var releaseDate: NSDate? {
166 | var date: NSDate? = nil
167 | if let dateString = self["releaseDate"].string {
168 | let dateFormatter = NSDateFormatter()
169 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss-SS:SS'"
170 | date = dateFormatter.dateFromString(dateString)
171 | }
172 | return date
173 | }
174 | var sellerName: String? { return self["sellerName"].string }
175 | var artistId: String? { return self["artistId"].string }
176 | var artistName: String? { return self["artistName"].string }
177 | var itunesDescription: String? { return self["itunesDescription"].string }
178 | var bundleId: String? { return self["bundleId"].string }
179 | var trackId: String? { return self["trackId"].int != nil ? String(self["trackId"].int!): nil }
180 | var trackName: String? { return self["trackName"].string }
181 | var primaryGenreName: String? { return self["primaryGenreName"].string }
182 | var primaryGenreId: String? { return self["primaryGenreId"].string }
183 | var releaseNotes: String? { return self["releaseNotes"].string }
184 | var minimumOsVersion: String? { return self["minimumOsVersion"].string }
185 | var averageUserRating: NSNumber { return NSNumber(float:(self["averageUserRating"].stringValue as NSString).floatValue) }
186 | var userRatingCount: NSNumber { return NSNumber(integer: Int(self["userRatingCount"].stringValue) ?? 0) }
187 |
188 | var isApplicationEntity: Bool{ return trackId != nil }
189 | }
190 |
--------------------------------------------------------------------------------
/ReviewManager/Models/ApplicationSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationSettings.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-25.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | let kEntityNameApplicationSettings = "ApplicationSettings"
12 |
13 | @objc(ApplicationSettings)
14 |
15 | class ApplicationSettings: NSManagedObject {
16 |
17 | @NSManaged var automaticUpdate: Bool
18 | @NSManaged var newReviews: NSNumber
19 | @NSManaged var reviewsUpdatedAt: NSDate?
20 | @NSManaged var nextUpdateAt: NSDate?
21 | @NSManaged var createdAt: NSDate
22 | @NSManaged var updatedAt: NSDate
23 | @NSManaged var application: Application
24 |
25 | // MARK: - Init & teardown
26 |
27 | override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
28 | super.init(entity: entity, insertIntoManagedObjectContext: context)
29 | }
30 |
31 | convenience init(insertIntoManagedObjectContext context: NSManagedObjectContext) {
32 | let entityDescription = NSEntityDescription.entityForName(kEntityNameApplicationSettings, inManagedObjectContext: context)
33 | self.init(entity: entityDescription!, insertIntoManagedObjectContext: context)
34 | }
35 |
36 | // MARK: - Class functions for create and insert and search
37 |
38 | class func get(application: Application, context: NSManagedObjectContext) -> ApplicationSettings? {
39 | let fetchRequest = NSFetchRequest(entityName: kEntityNameApplicationSettings)
40 | fetchRequest.predicate = NSPredicate(format: "application = %@", application)
41 | var result: [AnyObject]?
42 | do {
43 | result = try context.executeFetchRequest(fetchRequest)
44 | } catch let error as NSError {
45 | print(error)
46 | }
47 |
48 | return result?.last as? ApplicationSettings
49 | }
50 |
51 | class func new(application: Application, context: NSManagedObjectContext) -> ApplicationSettings {
52 | let settings = ApplicationSettings(insertIntoManagedObjectContext: context)
53 | settings.application = application;
54 | settings.automaticUpdate = true
55 | settings.createdAt = NSDate()
56 | settings.updatedAt = NSDate()
57 |
58 | return settings
59 | }
60 |
61 | class func getOrCreateNew(application: Application, context: NSManagedObjectContext) -> ApplicationSettings {
62 | if let settings = ApplicationSettings.get(application, context: context) {
63 | return settings
64 | } else {
65 | return ApplicationSettings.new(application, context: context)
66 | }
67 | }
68 |
69 | class func insertNewObjectIntoContext(context: NSManagedObjectContext) -> ApplicationSettings {
70 | return NSEntityDescription.insertNewObjectForEntityForName(kEntityNameApplicationSettings, inManagedObjectContext: context) as! ApplicationSettings
71 | }
72 | }
73 |
74 | // MARK: - ApplicationUpdater
75 |
76 | extension ApplicationSettings {
77 |
78 | var shouldUpdateReviews: Bool {
79 | if !automaticUpdate {
80 | return false
81 | }
82 | if let _ = reviewsUpdatedAt, nextUpdateAt = nextUpdateAt {
83 | return nextUpdateAt.compare(NSDate()) == .OrderedAscending
84 | } else {
85 | return true
86 | }
87 | }
88 |
89 | func increaseNewReviews() {
90 | let int = newReviews.integerValue
91 | newReviews = NSNumber(integer: int + 1)
92 | }
93 |
94 | func resetNewReviews() {
95 | newReviews = NSNumber(integer: 0)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/ReviewManager/Models/Review.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Review.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-08.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftyJSON
11 |
12 | let kEntityNameReview = "Review"
13 |
14 | @objc(Review)
15 | class Review: NSManagedObject {
16 |
17 | @NSManaged var apId: String
18 | @NSManaged var author: String
19 | @NSManaged var uri: String
20 | @NSManaged var title: String
21 | @NSManaged var content: String
22 | @NSManaged var version: String
23 | @NSManaged var rating: NSNumber
24 | @NSManaged var voteCount: NSNumber
25 | @NSManaged var voteSum: NSNumber
26 | @NSManaged var country: String
27 | @NSManaged var application: Application
28 | @NSManaged var createdAt: NSDate
29 | @NSManaged var updatedAt: NSDate
30 |
31 | // MARK: - init & teardown
32 |
33 | override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
34 | super.init(entity: entity, insertIntoManagedObjectContext: context)
35 | }
36 |
37 | convenience init(insertIntoManagedObjectContext context: NSManagedObjectContext) {
38 | let entityDescription = NSEntityDescription.entityForName(kEntityNameReview, inManagedObjectContext: context)
39 | self.init(entity: entityDescription!, insertIntoManagedObjectContext: context)
40 | }
41 |
42 | // MARK: - Class functions for create and insert
43 |
44 | class func getWithId(id: NSManagedObjectID, context: NSManagedObjectContext) -> Review? {
45 | var result: NSManagedObject?
46 | do {
47 | result = try context.existingObjectWithID(id)
48 | } catch let error as NSError {
49 | print(error)
50 | }
51 |
52 | return result as? Review
53 | }
54 |
55 | class func get(apId: String, context: NSManagedObjectContext) -> Review? {
56 | let fetchRequest = NSFetchRequest(entityName: kEntityNameReview)
57 | fetchRequest.predicate = NSPredicate(format: "apId = %@", apId)
58 | var result: [AnyObject]?
59 | do {
60 | result = try context.executeFetchRequest(fetchRequest)
61 | } catch let error as NSError {
62 | print(error)
63 | }
64 |
65 | return result?.last as? Review
66 | }
67 |
68 | class func new(apId: String, context: NSManagedObjectContext) -> Review {
69 | let review = Review(insertIntoManagedObjectContext: context)
70 | review.apId = apId;
71 | review.createdAt = NSDate()
72 | review.updatedAt = NSDate()
73 | return review
74 | }
75 |
76 | class func getOrCreateNew(apId: String, context: NSManagedObjectContext) -> Review {
77 | if let review = Review.get(apId, context: context) {
78 | return review
79 | } else {
80 | return Review.new(apId, context: context)
81 | }
82 | }
83 |
84 | class func insertNewObjectIntoContext(context: NSManagedObjectContext) -> Review {
85 | return NSEntityDescription.insertNewObjectForEntityForName(kEntityNameReview, inManagedObjectContext: context) as! Review
86 | }
87 | }
88 |
89 | // MARK: - Review extension of JSON
90 |
91 | extension Review {
92 |
93 | func updateWithJSON(json: JSON) {
94 |
95 | apId = json.reviewApID ?? ""
96 | author = json.reviewAuthor ?? ""
97 | uri = json.reviewUri ?? ""
98 | title = json.reviewTitle ?? ""
99 | content = json.reviewContent ?? ""
100 | version = json.reviewVersion ?? ""
101 | rating = json.reviewRating
102 | voteCount = json.reviewVoteCount
103 | voteSum = json.reviewVoteSum
104 | }
105 | }
106 |
107 | // MARK: - JSON extension of Review
108 |
109 | extension JSON {
110 | var reviewApID: String? { return self["id"]["label"].string }
111 | var reviewContent: String? { return self["content"]["label"].string }
112 | var reviewAuthor: String? { return self["author"]["name"]["label"].string }
113 | var reviewUri: String? { return self["author"]["uri"]["label"].string }
114 | var reviewTitle: String? { return self["title"]["label"].string }
115 | var reviewVersion: String? { return self["im:version"]["label"].string }
116 | var reviewRating: NSNumber { return NSNumber(integer: Int(self["im:rating"]["label"].stringValue) ?? 0) }
117 | var reviewVoteCount: NSNumber { return NSNumber(integer: Int(self["im:voteCount"]["label"].stringValue) ?? 0) }
118 | var reviewVoteSum: NSNumber { return NSNumber(float:(self["im:voteSum"]["label"].stringValue as NSString).floatValue) }
119 |
120 | var isReviewEntity: Bool { return (self.reviewContent != nil || self.reviewRating.integerValue > 0) }
121 | }
122 |
--------------------------------------------------------------------------------
/ReviewManager/PersistentStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PersistentStack.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-09.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AppKit
11 |
12 | let kDidAddReviewsNotification = "kDidAddReviewsNotification"
13 | let kDidUpdateApplicationNotification = "kDidUpdateApplicationNotification"
14 | let kDidUpdateApplicationSettingsNotification = "kDidUpdateApplicationSettingsNotification"
15 | let kDidUpdateReviewsNotification = "kDidUpdateReviewsNotification"
16 |
17 | class PersistentStack {
18 |
19 | var managedObjectContext: NSManagedObjectContext!
20 | var modelURL: NSURL
21 | var storeURL: NSURL
22 |
23 | init(storeURL: NSURL, modelURL: NSURL) {
24 | self.modelURL = modelURL
25 | self.storeURL = storeURL
26 | setupManagedObjectContexts()
27 | }
28 |
29 | func setupManagedObjectContexts() {
30 |
31 | managedObjectContext = setupManagedObjectContextWithConcurrencyType(.MainQueueConcurrencyType)
32 | managedObjectContext.undoManager = NSUndoManager()
33 |
34 | _ = NSNotificationCenter.defaultCenter().addObserverForName(NSManagedObjectContextDidSaveNotification, object: nil, queue: nil) { [weak self] notification in
35 | self?.managedObjectDidSave(notification)
36 | }
37 | }
38 |
39 | func managedObjectDidSave(notification: NSNotification) {
40 | let moc = managedObjectContext;
41 | if notification.object as? NSManagedObjectContext != moc {
42 | moc.performBlock({ [weak self] () -> Void in
43 |
44 | self?.mergeChangesFromSaveNotification(notification, intoContext: moc)
45 |
46 | var newReviews = Set()
47 | var updatedReviews = Set()
48 | var updatedApplications = Set()
49 | var updatedApplicationSettings = Set()
50 |
51 | if let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? NSSet {
52 | for object in insertedObjects {
53 | if let application = object as? Application {
54 | updatedApplications.insert(application.objectID)
55 | }
56 | if let review = object as? Review {
57 | newReviews.insert(review.objectID)
58 | updatedReviews.insert(review.objectID)
59 | }
60 | if let application = object as? ApplicationSettings {
61 | updatedApplicationSettings.insert(application.objectID)
62 | }
63 | }
64 | }
65 | if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? NSSet {
66 | for object in deletedObjects {
67 | if let application = object as? Application {
68 | updatedApplications.insert(application.objectID)
69 | }
70 | if let review = object as? Review {
71 | updatedReviews.insert(review.objectID)
72 | }
73 | if let settings = object as? ApplicationSettings {
74 | updatedApplicationSettings.insert(settings.objectID)
75 | }
76 | }
77 | }
78 | if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? NSSet {
79 | for object in updatedObjects {
80 | if let application = object as? Application {
81 | updatedApplications.insert(application.objectID)
82 | }
83 | if let review = object as? Review {
84 | updatedReviews.insert(review.objectID)
85 | }
86 | if let settings = object as? ApplicationSettings {
87 | updatedApplicationSettings.insert(settings.objectID)
88 | }
89 | }
90 | }
91 |
92 | if !newReviews.isEmpty {
93 | NSNotificationCenter.defaultCenter().postNotificationName(kDidAddReviewsNotification, object: newReviews)
94 | }
95 |
96 | if !updatedApplications.isEmpty {
97 | NSNotificationCenter.defaultCenter().postNotificationName(kDidUpdateApplicationNotification, object: updatedApplications)
98 | }
99 | if !updatedReviews.isEmpty {
100 | NSNotificationCenter.defaultCenter().postNotificationName(kDidUpdateReviewsNotification, object: newReviews)
101 | }
102 | if !updatedApplicationSettings.isEmpty {
103 | NSNotificationCenter.defaultCenter().postNotificationName(kDidUpdateApplicationSettingsNotification, object: updatedApplicationSettings)
104 | }
105 | })
106 | }
107 | }
108 |
109 | func mergeChangesFromSaveNotification(notification: NSNotification, intoContext context: NSManagedObjectContext) {
110 | // // NSManagedObjectContext's merge routine ignores updated objects which aren't
111 | // // currently faulted in. To force it to notify interested clients that such
112 | // // objects have been refreshed (e.g. NSFetchedResultsController) we need to
113 | // // force them to be faulted in ahead of the merge
114 |
115 | if let updatedObjects = notification.userInfo?[NSInsertedObjectsKey] as? NSSet {
116 | for anyObject in updatedObjects {
117 | if let managedObject = anyObject as? NSManagedObject {
118 | do {
119 | try context.existingObjectWithID(managedObject.objectID)
120 | } catch let error as NSError {
121 | print(error)
122 | } catch {
123 | fatalError()
124 | }
125 | }
126 | }
127 | }
128 | context.mergeChangesFromContextDidSaveNotification(notification)
129 | }
130 |
131 | func setupManagedObjectContextWithConcurrencyType(concurrencyType: NSManagedObjectContextConcurrencyType) -> NSManagedObjectContext {
132 |
133 | let managedObjectContext = NSManagedObjectContext(concurrencyType: concurrencyType)
134 | if let managedObjectModel = managedObjectModel() {
135 | managedObjectContext.persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
136 | do {
137 | try managedObjectContext.persistentStoreCoordinator?.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
138 | } catch let error as NSError {
139 | print(storeURL.path)
140 | print(error)
141 | }
142 | }
143 |
144 | return managedObjectContext;
145 | }
146 |
147 | func managedObjectModel() -> NSManagedObjectModel? {
148 | return NSManagedObjectModel(contentsOfURL: modelURL)
149 | }
150 |
151 | }
--------------------------------------------------------------------------------
/ReviewManager/ReviewManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // COReviewController.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-08.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AppKit
11 | import Ensembles
12 |
13 | let kSQLiteFileName = "db.sqlite"
14 |
15 | final class ReviewManager: NSObject {
16 |
17 | static let defaultManager = ReviewManager()
18 |
19 | private var persistentStack: PersistentStack!
20 | private var applicationUpdater: ApplicationUpdater!
21 | private var notificationsHandler: NotificationsHandler!
22 | private var persistentStoreEnsemble: CDEPersistentStoreEnsemble!
23 | private var cloudFileSystem: CDEICloudFileSystem!
24 |
25 | // MARK: - Init & teardown
26 |
27 | override init() {
28 | super.init()
29 | persistentStack = PersistentStack(storeURL: storeURL(), modelURL: modelURL())
30 | notificationsHandler = NotificationsHandler()
31 |
32 | cloudFileSystem = CDEICloudFileSystem(ubiquityContainerIdentifier: "iCloud.com.cocmoc.appreviews")
33 | persistentStoreEnsemble = CDEPersistentStoreEnsemble(ensembleIdentifier: "MainStore", persistentStoreURL: self.storeURL(), managedObjectModelURL: modelURL(), cloudFileSystem: cloudFileSystem)
34 | persistentStoreEnsemble.delegate = self
35 | }
36 |
37 | class func start() -> ReviewManager {
38 |
39 | let manager = ReviewManager.defaultManager
40 | manager.applicationUpdater = ApplicationUpdater()
41 |
42 | return manager
43 | }
44 |
45 | // MARK: - Core Data stack
46 |
47 | class func appUpdater() -> ApplicationUpdater {
48 | return ReviewManager.defaultManager.applicationUpdater
49 | }
50 |
51 | class func managedObjectContext() -> NSManagedObjectContext {
52 | return ReviewManager.defaultManager.persistentStack.managedObjectContext
53 | }
54 |
55 | class func backgroundObjectContext() -> NSManagedObjectContext {
56 | let context = ReviewManager.defaultManager.persistentStack.setupManagedObjectContextWithConcurrencyType(.PrivateQueueConcurrencyType)
57 | context.undoManager = nil
58 | context.mergePolicy = NSMergePolicy(mergeType: NSMergePolicyType.OverwriteMergePolicyType)
59 | return context
60 | }
61 |
62 | class func saveContext() {
63 | do {
64 | try ReviewManager.defaultManager.persistentStack.managedObjectContext.save()
65 | } catch let error as NSError {
66 | print(error)
67 | }
68 | }
69 |
70 | func storeURL() -> NSURL {
71 | var error: NSError?
72 | var message = "There was an error creating or loading the application's saved data."
73 |
74 | // Make sure the application files directory is there
75 | var propertiesOpt: [NSObject: AnyObject]?
76 | do {
77 | propertiesOpt = try self.applicationDocumentsDirectory.resourceValuesForKeys([NSURLIsDirectoryKey])
78 | } catch let error1 as NSError {
79 | error = error1
80 | }
81 | if let properties = propertiesOpt {
82 | if !properties[NSURLIsDirectoryKey]!.boolValue {
83 | message = "Expected a folder to store application data, found a file \(self.applicationDocumentsDirectory.path)."
84 | }
85 | } else if error?.code == NSFileReadNoSuchFileError {
86 | error = nil
87 | do {
88 | try NSFileManager.defaultManager().createDirectoryAtPath(self.applicationDocumentsDirectory.path!, withIntermediateDirectories: true, attributes: nil)
89 | } catch let error1 as NSError {
90 | error = error1
91 | }
92 | }
93 | if (error != nil) { print(message + " Error: \(error)") }
94 |
95 | return self.applicationDocumentsDirectory.URLByAppendingPathComponent(kSQLiteFileName)
96 | }
97 |
98 | lazy var applicationDocumentsDirectory: NSURL = {
99 | let urls = NSFileManager.defaultManager().URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)
100 | let appSupportURL = urls[urls.count - 1]
101 |
102 | let appName = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName") as? String ?? "App Reviews"
103 | return appSupportURL.URLByAppendingPathComponent(appName)
104 | }()
105 |
106 | func modelURL() -> NSURL {
107 | return NSBundle.mainBundle().URLForResource("AppReviews", withExtension: "momd")!
108 | }
109 | }
110 |
111 | // MARK: CDEPersistentStoreEnsembleDelegate
112 |
113 | extension ReviewManager: CDEPersistentStoreEnsembleDelegate {
114 |
115 | func persistentStoreEnsembleWillImportStore(ensemble: CDEPersistentStoreEnsemble!) {
116 | print("persistentStoreEnsembleWillImportStore")
117 | }
118 |
119 | func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, didSaveMergeChangesWithNotification notification: NSNotification!) {
120 | print("persistentStoreEnsemble didSaveMergeChangesWithNotification")
121 | ReviewManager.managedObjectContext().mergeChangesFromContextDidSaveNotification(notification)
122 | }
123 |
124 | // func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, globalIdentifiersForManagedObjects objects: [AnyObject]!) -> [AnyObject]! {
125 | // if let applications = objects as? [Application] {
126 | // return [applications]
127 | // } else {
128 | //
129 | // }
130 | // // return [objects valueForKeyPath:@"uniqueIdentifier"];
131 | // }
132 | }
--------------------------------------------------------------------------------
/ReviewManager/Timer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Timer.swift
3 | // App Reviews
4 | //
5 | // Created by Knut Inge Grosland on 2015-04-17.
6 | // Copyright (c) 2015 Cocmoc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class Timer {
12 |
13 | /// Closure will be called every time the timer fires
14 | typealias Closure = (timer: Timer) -> ()
15 |
16 | /// Parameters
17 | let closure: Closure
18 | let queue: dispatch_queue_t
19 | var isSuspended: Bool = true
20 |
21 | /// The default initializer
22 | init(queue: dispatch_queue_t, closure: Closure) {
23 | self.queue = queue
24 | self.closure = closure
25 | }
26 |
27 | /// Suspend the timer before it gets destroyed
28 | deinit {
29 | suspend()
30 | }
31 |
32 | /// This timer implementation uses Grand Central Dispatch sources
33 | lazy var source: dispatch_source_t = {
34 | dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue)
35 | }()
36 |
37 | /// Convenience class method that creates and start a timer
38 | class func repeatEvery(repeatEvery: Double, closure: Closure) -> Timer {
39 | let timer = Timer(queue: dispatch_get_global_queue(0, 0), closure: closure)
40 | timer.resume(0, `repeat`: repeatEvery, leeway: 0)
41 | return timer
42 | }
43 |
44 | /// Fire the timer by calling its closure
45 | func fire() {
46 | closure(timer: self)
47 | }
48 |
49 | /// Start or resume the timer with the specified double values
50 | func resume(start: Double, `repeat`: Double, leeway: Double) {
51 | let NanosecondsPerSecond = Double(NSEC_PER_SEC)
52 | resume(Int64(start * NanosecondsPerSecond), `repeat`: UInt64(`repeat` * NanosecondsPerSecond), leeway: UInt64(leeway * NanosecondsPerSecond))
53 | }
54 |
55 | /// Start or resume the timer with the specified integer values
56 | func resume(start: Int64, `repeat`: UInt64, leeway: UInt64) {
57 | if isSuspended {
58 | let startTime = dispatch_time(DISPATCH_TIME_NOW, start)
59 | dispatch_source_set_timer(source, startTime, `repeat`, leeway)
60 | dispatch_source_set_event_handler(source) { [weak self] in
61 | if let timer = self {
62 | timer.fire()
63 | }
64 | }
65 | dispatch_resume(source)
66 | isSuspended = false
67 | }
68 | }
69 |
70 | /// Suspend the timer
71 | func suspend() {
72 | if !isSuspended {
73 | dispatch_suspend(source)
74 | isSuspended = true
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Screenshots/add-application-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Screenshots/add-application-screen.png
--------------------------------------------------------------------------------
/Screenshots/appreviews-icon-100.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Screenshots/appreviews-icon-100.jpg
--------------------------------------------------------------------------------
/Screenshots/appreviews-icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Screenshots/appreviews-icon-512.png
--------------------------------------------------------------------------------
/Screenshots/review-screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Screenshots/review-screen.jpg
--------------------------------------------------------------------------------
/Screenshots/review-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knutigro/AppReviews/e9bf73b8cb92f3350666c9c6ece0f429883a2001/Screenshots/review-screen.png
--------------------------------------------------------------------------------
/travis/before_script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | brew unlink xctool
5 | brew update
6 | brew install xctool
7 |
8 | - gem install cocoapods -v '0.37.2'
--------------------------------------------------------------------------------
/travis/script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | xctool -workspace "App Reviews.xcworkspace" -scheme "App Reviews" build test -sdk macosx
5 |
6 |
--------------------------------------------------------------------------------