├── .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 | [![Build Status](https://travis-ci.org/knutigro/AppReviews.svg?branch=master)](https://travis-ci.org/knutigro/AppReviews) 3 | ![Platform](https://img.shields.io/badge/platform-osx-orange.svg) 4 | [![License](http://img.shields.io/:license-GNU-blue.svg)](https://github.com/knutigro/AppReviews/blob/develop/LICENSE) 5 | 6 | [![Icon](/Screenshots/appreviews-icon-100.jpg?raw=true)](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 | [![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](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 | ![Review-Screen](/Screenshots/review-screen.png?raw=true) 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 | --------------------------------------------------------------------------------