Repository: Kapeli/Dash-iOS
Branch: master
Commit: d47f6e99bacf
Files: 814
Total size: 45.4 MB
Directory structure:
gitextract_ynms4_oa/
├── .gitignore
├── CHANGELOG.md
├── Dash/
│ ├── Base.lproj/
│ │ └── Main.storyboard
│ ├── DHAppDelegate.h
│ ├── DHAppDelegate.m
│ ├── DHAppUpdateChecker.h
│ ├── DHAppUpdateChecker.m
│ ├── DHAppleAPIProtocol.h
│ ├── DHAppleAPIProtocol.m
│ ├── DHAppleActiveLanguage.h
│ ├── DHAppleActiveLanguage.m
│ ├── DHBlockProtocol.h
│ ├── DHBlockProtocol.m
│ ├── DHBrowserCell.xib
│ ├── DHBrowserTableView.h
│ ├── DHBrowserTableView.m
│ ├── DHBrowserTableViewCell.h
│ ├── DHBrowserTableViewCell.m
│ ├── DHCSS.h
│ ├── DHCSS.m
│ ├── DHCheatRepo.h
│ ├── DHCheatRepo.m
│ ├── DHCheatRepoList.h
│ ├── DHCheatRepoList.m
│ ├── DHDBNestedResultSorter.h
│ ├── DHDBNestedResultSorter.m
│ ├── DHDBResult.h
│ ├── DHDBResult.m
│ ├── DHDBResultSorter.h
│ ├── DHDBResultSorter.m
│ ├── DHDBSearchController.h
│ ├── DHDBSearchController.m
│ ├── DHDBSearcher.h
│ ├── DHDBSearcher.m
│ ├── DHDocset.h
│ ├── DHDocset.m
│ ├── DHDocsetBrowser.h
│ ├── DHDocsetBrowser.m
│ ├── DHDocsetBrowserViewModel.h
│ ├── DHDocsetBrowserViewModel.m
│ ├── DHDocsetDownloader.h
│ ├── DHDocsetDownloader.m
│ ├── DHDocsetIndexer.h
│ ├── DHDocsetIndexer.m
│ ├── DHDocsetManager.h
│ ├── DHDocsetManager.m
│ ├── DHDocsetTransferrer.h
│ ├── DHDocsetTransferrer.m
│ ├── DHEntryBrowser.h
│ ├── DHEntryBrowser.m
│ ├── DHFeed.h
│ ├── DHFeed.m
│ ├── DHFeedResult.h
│ ├── DHFeedResult.m
│ ├── DHFileDownload.h
│ ├── DHFileDownload.m
│ ├── DHImageCache.h
│ ├── DHImageCache.m
│ ├── DHJavaScript.h
│ ├── DHJavaScript.m
│ ├── DHJavaScriptBridge.h
│ ├── DHJavaScriptBridge.m
│ ├── DHLatencyTestResult.h
│ ├── DHLatencyTestResult.m
│ ├── DHLatencyTester.h
│ ├── DHLatencyTester.m
│ ├── DHLoadingCell.xib
│ ├── DHNavigationAnimator.h
│ ├── DHNavigationAnimator.m
│ ├── DHNavigationController.h
│ ├── DHNavigationController.m
│ ├── DHNestedViewController.h
│ ├── DHNestedViewController.m
│ ├── DHPreferences.h
│ ├── DHPreferences.m
│ ├── DHQueuedDB.h
│ ├── DHQueuedDB.m
│ ├── DHRemote.h
│ ├── DHRemote.m
│ ├── DHRemoteBrowser.h
│ ├── DHRemoteBrowser.m
│ ├── DHRemoteImage.h
│ ├── DHRemoteImage.m
│ ├── DHRemoteProtocol.h
│ ├── DHRemoteProtocol.m
│ ├── DHRemoteServer.h
│ ├── DHRemoteServer.m
│ ├── DHRepo.h
│ ├── DHRepo.m
│ ├── DHRepoCell.xib
│ ├── DHRepoTableView.h
│ ├── DHRepoTableView.m
│ ├── DHRepoTableViewCell.h
│ ├── DHRepoTableViewCell.m
│ ├── DHRightDetailLabel.h
│ ├── DHRightDetailLabel.m
│ ├── DHSearchDisplayController.h
│ ├── DHSearchDisplayController.m
│ ├── DHSplitViewController.h
│ ├── DHSplitViewController.m
│ ├── DHTarixIndex.h
│ ├── DHTarixIndex.m
│ ├── DHTarixProtocol.h
│ ├── DHTarixProtocol.m
│ ├── DHTocBrowser.h
│ ├── DHTocBrowser.m
│ ├── DHTransferFeed.h
│ ├── DHTransferFeed.m
│ ├── DHType.h
│ ├── DHType.m
│ ├── DHTypeBrowser.h
│ ├── DHTypeBrowser.m
│ ├── DHTypes.h
│ ├── DHTypes.m
│ ├── DHUnarchiver.h
│ ├── DHUnarchiver.m
│ ├── DHUserRepo.h
│ ├── DHUserRepo.m
│ ├── DHUserRepoList.h
│ ├── DHUserRepoList.m
│ ├── DHWebProgressView.h
│ ├── DHWebProgressView.m
│ ├── DHWebView.h
│ ├── DHWebView.m
│ ├── DHWebViewController.h
│ ├── DHWebViewController.m
│ ├── DHWindow.h
│ ├── DHWindow.m
│ ├── Dash iOS.xcodeproj/
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── Dash iOS.xccheckout
│ │ └── Dash iOS.xcscmblueprint
│ ├── Dash-Info.plist
│ ├── Dash-Prefix.pch
│ ├── Defaults.plist
│ ├── FMDatabase.h
│ ├── FMDatabase.m
│ ├── FMDatabaseAdditions.h
│ ├── FMDatabaseAdditions.m
│ ├── FMDatabasePool.h
│ ├── FMDatabasePool.m
│ ├── FMDatabaseQueue.h
│ ├── FMDatabaseQueue.m
│ ├── FMResultSet.h
│ ├── FMResultSet.m
│ ├── Images.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Empty Placeholders/
│ │ │ ├── placeholder_docsets.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── placeholder_transfer.imageset/
│ │ │ └── Contents.json
│ │ ├── Platforms/
│ │ │ ├── Contents.json
│ │ │ ├── Dash.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Google.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Mac.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Other.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── SproutCore.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Stack Overflow.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── actionscript.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── akka.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── android.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── angular.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── angulardart.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── angularjs.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── angularts.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ansible.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── apache.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── apple.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── applescript.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── arduino.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── awesome.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── awsjs.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── backbone.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── bash.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── boost.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── bootstrap.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── bourbon.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── c.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cakephp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cappuccino.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cf.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── chai.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cheatsheet.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── chef.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cljlib.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── clojure.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cmake.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cocoadocs_platform.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cocoapods.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cocos2d.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cocos2dx.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── codeigniter.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── coffee.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── compass.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cordova.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── corona.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── couchdb.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cpp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── craft.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── css.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cvc.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cvcpp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cvj.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cvp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── d3.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── dartdocs.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── dartlang.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── django.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── docker.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── doctrine.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── dojo.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── dom.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── drupal.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ee.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── elasticsearch.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── elisp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── elixir.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ember.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── emmet.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── erlang.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── express.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── extjs.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── flask.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── foundation.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── git.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── github.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── gl2.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── gl3.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── gl4.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── glib.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── go.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── godoc.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── gradle.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── grails.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── groovy.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── grunt.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── gulp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── hackage.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── haml.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── handlebars.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── haskell.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── hex.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── html.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ionic.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── iphone.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jQuery.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jade.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jasmine.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── java.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── javadoc.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── javafx.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── javascript.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jee6.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jee7.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jee8.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jekyll.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jinja.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── joomla.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jquerym.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── jqueryui.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── julia.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── knockout.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── kobold2d.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── laravel.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── latex.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── less.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── linux.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── lisp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── lodash.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── lua.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── manPages.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── marionette.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── markdown.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── matlab.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── matplotlib.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── meteor.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── mocha.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── moment.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── mongodb.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── mongoose.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── mono.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── moo.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── mysql.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── neat.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── net.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nginx.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nodejs.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── numpy.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ocaml.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── opencv.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── pandas.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── perl.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── phalcon.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── phonegap.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── php.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── phpp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── phpunit.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── playjava.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── playscala.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── polymerdart.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── processing.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── prototype.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── psql.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── pug.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── puppet.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── python.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── qt.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── r.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── racket.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── rails.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── react.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── redis.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── require.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ruby.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── rubyGems.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── rubymotion.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── rust.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sails.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── salt.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sass.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── scala.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── scaladoc.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── scipy.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── semantic.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sencha.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sinon.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── smarty.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sooffline.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── soonline.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sparrow.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── spring.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sqlalchemy.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── sqlite.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── statamic.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── stylus.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── susy.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── svg.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── swift.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── symfony.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── tcl.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── titanium.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── tornado.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── tvos.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── twig.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── twisted.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── typescript.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── typo3.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── underscore.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── unity3d.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── vagrant.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── vim.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── vsphere.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── vue.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── watchos.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── wordpress.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── xamarin.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── xojo.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── xslt.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── xul.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── yii.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── yui.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── zend.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── zepto.imageset/
│ │ │ └── Contents.json
│ │ ├── Preferences/
│ │ │ ├── Contents.json
│ │ │ ├── cheat_repo.imageset/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── cheat_repo.psd
│ │ │ │ └── cheat_repo@2x.psd
│ │ │ ├── main_repo.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── transfer_repo.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── user_repo.imageset/
│ │ │ ├── Contents.json
│ │ │ ├── user_repo.psd
│ │ │ └── user_repo@2x.psd
│ │ ├── Repos/
│ │ │ ├── checkmark.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── download_button_arrow.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── download_button_background.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── error.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── transfer_button.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── trash.imageset/
│ │ │ └── Contents.json
│ │ ├── Types/
│ │ │ ├── Abbreviation.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Alias.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Annotation.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Attribute.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Axiom.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Binding.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Block.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Bookmark.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Builtin.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Callback.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Category.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Class.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Collection.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Column.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Command.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Component.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Constant.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Constructor.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── Control Structure.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Conversion.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Data Source.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Database.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Decorator.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Define.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Delegate.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── DeletedSnippet.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Device.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Diagram.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Directive.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Element.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Entry.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Enum.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Environment.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Error.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Event.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Exception.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Expression.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Extension.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Field.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── File.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Filter.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Flag.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Foreign Key.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Framework.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Function.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Global.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Glossary.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Guide.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Handler.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Helper.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Hook.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Index.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Indirection.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Inductive.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Instance.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Instruction.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Interface.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Iterator.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Keyword.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Kind.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Lemma.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Library.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Literal.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Macro.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Member.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Message.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Method.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Mixin.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Modifier.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Module.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Namespace.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── NewSnippet.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Node.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Notation.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Object.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Operator.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Option.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Package.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Parameter.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Pattern.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Pipe.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Plugin.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Procedure.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Projection.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Property.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Protocol.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Provider.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Provisioner.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Query.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Record.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Reference.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Register.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Relationship.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Report.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Request.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Resource.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Role.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Sample.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Schema.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Script.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Section.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Sender.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Service.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Setting.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Shortcut.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Signature.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Snippet.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Special Form.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── State.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Statement.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Struct.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Style.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Subroutine.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Syntax.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Table.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Tactic.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Tag.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Template.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Test.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Trait.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Trigger.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Type.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Union.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Unknown.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Value.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Variable.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Variant.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── View.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Web.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── WebSearch.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Widget.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Word.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── _DashAnnotations.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── _Struct.imageset/
│ │ │ └── Contents.json
│ │ └── WebView/
│ │ ├── back.imageset/
│ │ │ └── Contents.json
│ │ ├── collapse.imageset/
│ │ │ └── Contents.json
│ │ ├── expand.imageset/
│ │ │ └── Contents.json
│ │ ├── forward.imageset/
│ │ │ └── Contents.json
│ │ ├── tocMenu.imageset/
│ │ │ └── Contents.json
│ │ ├── zoomIn.imageset/
│ │ │ └── Contents.json
│ │ └── zoomOut.imageset/
│ │ └── Contents.json
│ ├── Launch.xib
│ ├── NSArray+DHUtils.h
│ ├── NSArray+DHUtils.m
│ ├── NSData+DHUtils.h
│ ├── NSData+DHUtils.m
│ ├── NSFileManager+DHUtils.h
│ ├── NSFileManager+DHUtils.m
│ ├── NSObject+DHUtils.h
│ ├── NSObject+DHUtils.m
│ ├── NSString+DHUtils.h
│ ├── NSString+DHUtils.m
│ ├── NSString+GTM.h
│ ├── NSString+GTM.m
│ ├── NSTimer+DHUtils.h
│ ├── NSTimer+DHUtils.m
│ ├── NSURL+DHUtils.h
│ ├── NSURL+DHUtils.m
│ ├── Settings.bundle/
│ │ ├── Cocoa_Pods_Acknowledgements.plist
│ │ ├── License.plist
│ │ ├── Other_Acknowledgements.plist
│ │ ├── Root.plist
│ │ └── en.lproj/
│ │ └── Root.strings
│ ├── UIButton+DHUtils.h
│ ├── UIButton+DHUtils.m
│ ├── UITableView+DHUtils.h
│ ├── UITableView+DHUtils.m
│ ├── UIView+DHUtils.h
│ ├── UIView+DHUtils.m
│ ├── UIViewController+DHUtils.h
│ ├── UIViewController+DHUtils.m
│ ├── apple.js
│ ├── archive.h
│ ├── archive_entry.h
│ ├── cocoa.js
│ ├── en.lproj/
│ │ └── InfoPlist.strings
│ ├── hash_change_notifier.js
│ ├── home.html
│ ├── libarchive.a
│ ├── main.m
│ ├── msdn.js
│ ├── on_page_load.js
│ ├── ruby.js
│ ├── scroll_to_current_anchor.js
│ ├── style.css
│ ├── whitespace_tokenizer.c
│ └── whitespace_tokenizer.h
├── Dash iOS.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ ├── Dash iOS.xccheckout
│ ├── Dash iOS.xcscmblueprint
│ └── IDEWorkspaceChecks.plist
├── Frameworks/
│ └── Apple_Docs_Framework.framework/
│ ├── Apple_Docs_Framework
│ ├── Headers/
│ │ ├── Apple_Docs_Framework.h
│ │ ├── DHCommandLineParser.h
│ │ ├── DHTester.h
│ │ ├── DHViewer.h
│ │ ├── DHViewerHelper.h
│ │ └── DHXcodeHelper.h
│ ├── Info.plist
│ ├── Modules/
│ │ └── module.modulemap
│ └── prism.js
├── Icon/
│ └── Icon.psd
├── LICENSE
├── Modified Pods/
│ ├── DTBonjour/
│ │ ├── .gitignore
│ │ ├── .travis.yml
│ │ ├── AppledocSettings.plist
│ │ ├── Core/
│ │ │ ├── DTBonjour-Prefix.pch
│ │ │ └── Source/
│ │ │ ├── DTBonjourDataChunk.h
│ │ │ ├── DTBonjourDataChunk.m
│ │ │ ├── DTBonjourDataConnection.h
│ │ │ ├── DTBonjourDataConnection.m
│ │ │ ├── DTBonjourServer.h
│ │ │ ├── DTBonjourServer.m
│ │ │ ├── NSScanner+DTBonjour.h
│ │ │ └── NSScanner+DTBonjour.m
│ │ ├── DTBonjour.podspec
│ │ ├── DTBonjour.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ └── xcshareddata/
│ │ │ └── xcschemes/
│ │ │ ├── DTBonjour (OS X).xcscheme
│ │ │ └── DTBonjour (iOS).xcscheme
│ │ ├── Documentation/
│ │ │ └── Change Log-template.markdown
│ │ ├── LICENSE
│ │ ├── build.gradle
│ │ ├── coveralls.rb
│ │ └── readme.markdown
│ ├── KissXML-5.1.2/
│ │ ├── .gitignore
│ │ ├── .hgignore
│ │ ├── .travis.yml
│ │ ├── KissXML/
│ │ │ ├── Additions/
│ │ │ │ ├── DDXMLElementAdditions.h
│ │ │ │ └── DDXMLElementAdditions.m
│ │ │ ├── Categories/
│ │ │ │ ├── NSString+DDXML.h
│ │ │ │ └── NSString+DDXML.m
│ │ │ ├── DDXML.h
│ │ │ ├── DDXML.swift
│ │ │ ├── DDXMLDocument.h
│ │ │ ├── DDXMLDocument.m
│ │ │ ├── DDXMLElement.h
│ │ │ ├── DDXMLElement.m
│ │ │ ├── DDXMLNode.h
│ │ │ ├── DDXMLNode.m
│ │ │ ├── KissXML.h
│ │ │ └── Private/
│ │ │ └── DDXMLPrivate.h
│ │ ├── KissXML.podspec
│ │ ├── LICENSE.txt
│ │ ├── README.markdown
│ │ └── Tests/
│ │ ├── Mac/
│ │ │ ├── KissXML.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ └── contents.xcworkspacedata
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── KissXMLTests.xcscheme
│ │ │ ├── KissXML.xcworkspace/
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── KissXMLTests/
│ │ │ │ └── Info.plist
│ │ │ └── Podfile
│ │ ├── Shared/
│ │ │ └── KissXMLTests.m
│ │ ├── Swift/
│ │ │ ├── KissXML.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ └── contents.xcworkspacedata
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── KissXMLSwiftTests.xcscheme
│ │ │ ├── KissXML.xcworkspace/
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── KissXMLSwiftTests/
│ │ │ │ ├── Info.plist
│ │ │ │ └── KissXMLSwiftTests.swift
│ │ │ └── Podfile
│ │ ├── Swift-iOS/
│ │ │ ├── KissXML.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ └── contents.xcworkspacedata
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── KissXMLSwiftTests.xcscheme
│ │ │ ├── KissXML.xcworkspace/
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── KissXMLSwiftTests/
│ │ │ │ ├── Info.plist
│ │ │ │ └── KissXMLSwiftTests.swift
│ │ │ └── Podfile
│ │ ├── iOS/
│ │ │ ├── KissXML.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ └── contents.xcworkspacedata
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── KissXMLTests.xcscheme
│ │ │ ├── KissXML.xcworkspace/
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── KissXMLTests/
│ │ │ │ └── Info.plist
│ │ │ └── Podfile
│ │ └── iOS-static/
│ │ ├── KissXML.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ └── contents.xcworkspacedata
│ │ │ └── xcshareddata/
│ │ │ └── xcschemes/
│ │ │ └── KissXMLTests.xcscheme
│ │ ├── KissXML.xcworkspace/
│ │ │ └── contents.xcworkspacedata
│ │ ├── KissXMLTests/
│ │ │ └── Info.plist
│ │ └── Podfile
│ └── MRProgress/
│ ├── LICENSE
│ ├── MRProgress.podspec
│ ├── README.md
│ └── src/
│ ├── Blur/
│ │ ├── MRBlurView.h
│ │ ├── MRBlurView.m
│ │ ├── UIImage+MRImageEffects.h
│ │ └── UIImage+MRImageEffects.m
│ ├── Components/
│ │ ├── MRActivityIndicatorView.h
│ │ ├── MRActivityIndicatorView.m
│ │ ├── MRCircularProgressView.h
│ │ ├── MRCircularProgressView.m
│ │ ├── MRIconView.h
│ │ ├── MRIconView.m
│ │ ├── MRNavigationBarProgressView.h
│ │ ├── MRNavigationBarProgressView.m
│ │ ├── MRProgressOverlayView.h
│ │ ├── MRProgressOverlayView.m
│ │ ├── MRProgressView.h
│ │ ├── MRProgressView.m
│ │ ├── MRStopButton.h
│ │ ├── MRStopButton.m
│ │ └── MRStopableView.h
│ ├── MRProgress.h
│ └── Utils/
│ └── MRProgressHelper.h
├── Podfile
├── Pods/
│ ├── AutoCoding/
│ │ ├── AutoCoding/
│ │ │ ├── AutoCoding.h
│ │ │ └── AutoCoding.m
│ │ ├── LICENCE.md
│ │ └── README.md
│ ├── DZNEmptyDataSet/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── Source/
│ │ ├── UIScrollView+EmptyDataSet.h
│ │ └── UIScrollView+EmptyDataSet.m
│ ├── GZIP/
│ │ ├── GZIP/
│ │ │ ├── GZIP.h
│ │ │ ├── NSData+GZIP.h
│ │ │ └── NSData+GZIP.m
│ │ ├── LICENCE.md
│ │ └── README.md
│ ├── HockeySDK/
│ │ └── HockeySDK-iOS/
│ │ ├── HockeySDK.embeddedframework/
│ │ │ ├── HockeySDK.framework/
│ │ │ │ ├── Headers/
│ │ │ │ │ ├── BITAuthenticator.h
│ │ │ │ │ ├── BITCrashAttachment.h
│ │ │ │ │ ├── BITCrashDetails.h
│ │ │ │ │ ├── BITCrashManager.h
│ │ │ │ │ ├── BITCrashManagerDelegate.h
│ │ │ │ │ ├── BITCrashMetaData.h
│ │ │ │ │ ├── BITHockeyAttachment.h
│ │ │ │ │ ├── BITHockeyBaseManager.h
│ │ │ │ │ ├── BITHockeyBaseViewController.h
│ │ │ │ │ ├── BITHockeyManager.h
│ │ │ │ │ ├── BITHockeyManagerDelegate.h
│ │ │ │ │ ├── BITMetricsManager.h
│ │ │ │ │ ├── BITStoreUpdateManager.h
│ │ │ │ │ ├── BITStoreUpdateManagerDelegate.h
│ │ │ │ │ ├── BITUpdateManager.h
│ │ │ │ │ ├── BITUpdateManagerDelegate.h
│ │ │ │ │ ├── BITUpdateViewController.h
│ │ │ │ │ ├── HockeySDK.h
│ │ │ │ │ ├── HockeySDKEnums.h
│ │ │ │ │ ├── HockeySDKFeatureConfig.h
│ │ │ │ │ └── HockeySDKNullability.h
│ │ │ │ ├── HockeySDK
│ │ │ │ └── Modules/
│ │ │ │ └── module.modulemap
│ │ │ └── HockeySDKResources.bundle/
│ │ │ ├── de.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── en.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── es.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── fa.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── fr.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── hr.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── hu.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── it.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── ja.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── nb.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── nl.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── pt-PT.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── pt.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── ru.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ ├── tr.lproj/
│ │ │ │ └── HockeySDK.strings
│ │ │ └── zh-Hans.lproj/
│ │ │ └── HockeySDK.strings
│ │ ├── LICENSE
│ │ └── README.md
│ ├── JGMethodSwizzler/
│ │ ├── JGMethodSwizzler/
│ │ │ ├── JGMethodSwizzler.h
│ │ │ └── JGMethodSwizzler.m
│ │ └── README.md
│ ├── Local Podspecs/
│ │ ├── DTBonjour.podspec.json
│ │ ├── DZNEmptyDataSet.podspec.json
│ │ ├── KissXML.podspec.json
│ │ └── MRProgress.podspec.json
│ ├── NSTimer-Blocks/
│ │ ├── NSTimer+Blocks.h
│ │ ├── NSTimer+Blocks.m
│ │ └── README.md
│ ├── Pods.xcodeproj/
│ │ └── project.pbxproj
│ ├── Reachability/
│ │ ├── LICENCE.txt
│ │ ├── README.md
│ │ ├── Reachability.h
│ │ └── Reachability.m
│ ├── SAMKeychain/
│ │ ├── LICENSE
│ │ ├── Readme.markdown
│ │ ├── Sources/
│ │ │ ├── SAMKeychain.h
│ │ │ ├── SAMKeychain.m
│ │ │ ├── SAMKeychainQuery.h
│ │ │ └── SAMKeychainQuery.m
│ │ └── Support/
│ │ └── SAMKeychain.bundle/
│ │ └── en.lproj/
│ │ └── SAMKeychain.strings
│ ├── Target Support Files/
│ │ ├── AutoCoding/
│ │ │ ├── AutoCoding-dummy.m
│ │ │ ├── AutoCoding-prefix.pch
│ │ │ └── AutoCoding.xcconfig
│ │ ├── DTBonjour/
│ │ │ ├── DTBonjour-dummy.m
│ │ │ ├── DTBonjour-prefix.pch
│ │ │ └── DTBonjour.xcconfig
│ │ ├── DZNEmptyDataSet/
│ │ │ ├── DZNEmptyDataSet-dummy.m
│ │ │ ├── DZNEmptyDataSet-prefix.pch
│ │ │ └── DZNEmptyDataSet.xcconfig
│ │ ├── GZIP/
│ │ │ ├── GZIP-dummy.m
│ │ │ ├── GZIP-prefix.pch
│ │ │ └── GZIP.xcconfig
│ │ ├── HockeySDK/
│ │ │ └── ResourceBundle-HockeySDKResources-Info.plist
│ │ ├── JGMethodSwizzler/
│ │ │ ├── JGMethodSwizzler-dummy.m
│ │ │ ├── JGMethodSwizzler-prefix.pch
│ │ │ └── JGMethodSwizzler.xcconfig
│ │ ├── KissXML/
│ │ │ ├── KissXML-dummy.m
│ │ │ ├── KissXML-prefix.pch
│ │ │ └── KissXML.xcconfig
│ │ ├── MRProgress/
│ │ │ ├── MRProgress-dummy.m
│ │ │ ├── MRProgress-prefix.pch
│ │ │ └── MRProgress.xcconfig
│ │ ├── NSTimer-Blocks/
│ │ │ ├── NSTimer-Blocks-dummy.m
│ │ │ ├── NSTimer-Blocks-prefix.pch
│ │ │ └── NSTimer-Blocks.xcconfig
│ │ ├── Pods-Dash/
│ │ │ ├── Pods-Dash-acknowledgements.markdown
│ │ │ ├── Pods-Dash-acknowledgements.plist
│ │ │ ├── Pods-Dash-dummy.m
│ │ │ ├── Pods-Dash-frameworks.sh
│ │ │ ├── Pods-Dash-resources.sh
│ │ │ ├── Pods-Dash.debug.xcconfig
│ │ │ └── Pods-Dash.release.xcconfig
│ │ ├── Pods-Dash-Dash App Store/
│ │ │ ├── Pods-Dash-Dash App Store-acknowledgements.markdown
│ │ │ ├── Pods-Dash-Dash App Store-acknowledgements.plist
│ │ │ ├── Pods-Dash-Dash App Store-dummy.m
│ │ │ ├── Pods-Dash-Dash App Store-frameworks.sh
│ │ │ ├── Pods-Dash-Dash App Store-resources.sh
│ │ │ ├── Pods-Dash-Dash App Store.debug.xcconfig
│ │ │ └── Pods-Dash-Dash App Store.release.xcconfig
│ │ ├── Reachability/
│ │ │ ├── Reachability-dummy.m
│ │ │ ├── Reachability-prefix.pch
│ │ │ └── Reachability.xcconfig
│ │ ├── SAMKeychain/
│ │ │ ├── SAMKeychain-dummy.m
│ │ │ ├── SAMKeychain-prefix.pch
│ │ │ └── SAMKeychain.xcconfig
│ │ ├── UIActionSheet+Blocks/
│ │ │ ├── UIActionSheet+Blocks-dummy.m
│ │ │ ├── UIActionSheet+Blocks-prefix.pch
│ │ │ └── UIActionSheet+Blocks.xcconfig
│ │ └── UIAlertView+Blocks/
│ │ ├── UIAlertView+Blocks-dummy.m
│ │ ├── UIAlertView+Blocks-prefix.pch
│ │ └── UIAlertView+Blocks.xcconfig
│ ├── UIActionSheet+Blocks/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── UIActionSheet+Blocks.h
│ │ └── UIActionSheet+Blocks.m
│ └── UIAlertView+Blocks/
│ ├── LICENSE
│ ├── README.md
│ ├── UIAlertView+Blocks.h
│ └── UIAlertView+Blocks.m
└── README.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xcuserstate
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
fastlane/report.xml
fastlane/screenshots
#Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
================================================
FILE: CHANGELOG.md
================================================
# Master
* Nothing yet.
# 1.8.13
* Added support for the Xcode 11 docs - [b052c97](https://github.com/Kapeli/Dash-iOS/commit/b052c970b6536cc12ce5ab216f723466eff9899c)
# 1.8.12
* Added support for the Xcode 10.2 docs - [2065d5b](https://github.com/Kapeli/Dash-iOS/commit/2065d5b4995d7ab4b35a25a3bd12ad305a4a106d)
# 1.8.11
* Added support for the Xcode 10.1 docs - [b6d3e4f](https://github.com/Kapeli/Dash-iOS/commit/b6d3e4f3d53c4ac77628e3142b656aaa7c0ab966)
# 1.8.10
* Added support for the Xcode 10 docs - [2aa2284](https://github.com/Kapeli/Dash-iOS/commit/2aa228480092f311b6ae809cfedff2ad5701e2cf)
* Added Java SE11 docset - [55ca952](https://github.com/Kapeli/Dash-iOS/commit/55ca952e25edfbf82100299f9c1f9d7ba6eb377a)
# 1.8.9
* Added the Swift docset back - [ec51356](https://github.com/Kapeli/Dash-iOS/commit/ec51356e95ac7a0aedbd94e648771578174db35a)
* Fixed an UI issue which occurred only in landscape mode on iPhone X. Thanks to [@qinyuhang](https://github.com/qinyuhang) for reporting the issue and [@DmytriE](https://github.com/DmytriE) for fixing it - [#80](https://github.com/Kapeli/Dash-iOS/pull/80)
# 1.8.8
* Apple API Reference docset: added support for Xcode 9.4 and Xcode 10 - [f29d3f0](https://github.com/Kapeli/Dash-iOS/commit/f29d3f01bb2030b81e311f74e45f5743568e53e6) and [0acc05c](https://github.com/Kapeli/Dash-iOS/commit/0acc05c01aadb427a51579c2b7a048a32e825bdf)
# 1.8.7
* Improved the way Dash selects the best server to connect to - [e122d05](https://github.com/Kapeli/Dash-iOS/commit/e122d05120aab70b2545f5300806a564276ee6d8)
* Added Java SE10 docset - [c9d62b1](https://github.com/Kapeli/Dash-iOS/commit/c9d62b169fe9aa8945d8d0af7034e57928473741)
* Removed JavaFX docset as it's now part of the Java SE docset - [c9d62b1](https://github.com/Kapeli/Dash-iOS/commit/c9d62b169fe9aa8945d8d0af7034e57928473741)
* Docset index pages can now scroll to a specific section within the page - [091f04a](https://github.com/Kapeli/Dash-iOS/commit/091f04a10754a4256bd5e35545b3532440e63571)
# 1.8.6
* Fixed an issue with the Apple API Reference docset which caused some pages to not load - [bac544a](https://github.com/Kapeli/Dash-iOS/commit/bac544af11f3823bb246989201623b3bee3b8a39)
* Updated Stylus docset icon - [0ad39d3](https://github.com/Kapeli/Dash-iOS/commit/0ad39d3363ead03d430f2cd8dcfa1007c93e5697)
# 1.8.5
* Added Apple API Reference docset support for Xcode 9.3 - [c585578](https://github.com/Kapeli/Dash-iOS/commit/c585578d0ab965dd3481f4c06aff6c320873d5f3)
# 1.8.4
* Fixed an issue which caused the Apple API Reference docset to not be able to display some pages. Thanks to [@philosopherdog](https://github.com/philosopherdog) for reporting the issue - [#71](https://github.com/Kapeli/Dash-iOS/issues/71)
* Fixed an issue which caused docset alphabetical sorting to be case-sensitive. Thanks to [@ewalkie](https://github.com/ewalkie) for reporting the issue - [#72](https://github.com/Kapeli/Dash-iOS/issues/72)
# 1.8.3
* Added support for sorting docsets alphabetically in the docset browser. Thanks to [@waffleboot](https://github.com/waffleboot) for the great work on this - [#69](https://github.com/Kapeli/Dash-iOS/pull/69)
* Added support for Xcode 9.3 docs - [73cadfb](https://github.com/Kapeli/Dash-iOS/commit/73cadfbcbb6e172ec8d12c2ef222a64160e4e42d)
# 1.8.2
* Fixed crash at launch for release build - [c0d2582](https://github.com/Kapeli/Dash-iOS/commit/c0d2582e70dbeec560c03781727fd8dcf95b9e7d)
# 1.8.1
* Fixed Apple API Reference docset transfer issues - [#66](https://github.com/Kapeli/Dash-iOS/issues/66)
# 1.8.0
* Fixed docset index page button on iOS 11 - [7fd09e8](https://github.com/Kapeli/Dash-iOS/commit/7fd09e8cae3b981aa75662ef3d19111a3ab2039a)
* Fixed an issue which caused the Swift docset to still appear in Dash, although it was removed - [ca9c9f6](https://github.com/Kapeli/Dash-iOS/commit/ca9c9f64daf9eac30c4dcc000f99240a424bb123)
* Fixed an issue which caused the search results table to be inset on iOS 11 - [d022d88](https://github.com/Kapeli/Dash-iOS/commit/d022d888e21a37e54a9960239689cac54bb7ef5b)
* Fixed an issue which caused search results to not be highlighted correctly - [aea602e](https://github.com/Kapeli/Dash-iOS/commit/aea602e9b5292c110f6cb934f892349a3290689d)
* Fixed access to UI on background thread - [90b1875](https://github.com/Kapeli/Dash-iOS/commit/90b1875f3728f5ca4485693ec8209cf8342cecfe)
* Fixed an issue which caused extra table row separators to appear above the search results table - [313893d](https://github.com/Kapeli/Dash-iOS/commit/313893ddeddb10b029d7bb2c324867a09a127946)
* Fixed Settings button sometimes appearing faded - [2bb42fe](https://github.com/Kapeli/Dash-iOS/commit/2bb42fe2ee51a3fabadb921100da6b0c7674efbc)
* Consolidated the OpenCV C, C++, Python and Java docsets into a single OpenCV docset - [9af12ee](https://github.com/Kapeli/Dash-iOS/commit/9af12ee33f4d60de14d4bbb0a0741be61296e2b0)
# 1.7.0
* Added support for adding docsets using the "Open in..." menu. Thanks to [@insightmind](https://github.com/insightmind) for the great work on this - [#52](https://github.com/Kapeli/Dash-iOS/pull/52)
* Added Java SE9 docset - [7c727c2](https://github.com/Kapeli/Dash-iOS/commit/7c727c2d30d41c0f37a4588510a804e4300b8c61)
* Added Java EE8 docset - [61e2df7](https://github.com/Kapeli/Dash-iOS/commit/61e2df74f955bcf22ff6611be1ff0f6e45f6024a)
* Added AngularJS docset (now separate from the Angular docset) - [b0ef193](https://github.com/Kapeli/Dash-iOS/commit/b0ef1936b71b026baa92e76371331be26c1f32dd)
* Removed Swift docset. Swift docs can be found in the Apple API Reference docset - [88913a6](https://github.com/Kapeli/Dash-iOS/commit/88913a6236c8c3c3874da63b300930496658637e)
* Fixed an AirDrop issue. Thanks to [@ClementPadovani](https://github.com/ClementPadovani) for the fix - [#61](https://github.com/Kapeli/Dash-iOS/pull/61)
* Added Glossary, Control Structure, Expression, Handler, Iterator, Widget, Block, Template types - [a94006b](https://github.com/Kapeli/Dash-iOS/commit/a94006bc39996c69d168f9c2d8f94b0e37c31ac6)
# 1.6.3
* Fixed an iPad-only crash which occurred in Settings while going into split view mode with the search field active - [36cf36d](https://github.com/Kapeli/Dash-iOS/commit/36cf36df40619ebfae903e39af4ea836e26fdc42)
# 1.6.2
* Fixed an issue which caused the iOS remote feature to sometimes not pair correctly - [9f9dd6c](https://github.com/Kapeli/Dash-iOS/commit/9f9dd6c8b5761b28899dcae01f828888ab9011d8)
* Consolidated all Angular docsets into one - [56fda1b](https://github.com/Kapeli/Dash-iOS/commit/56fda1b4fa94fa910e377004ba7988ecc5e389eb)
* Fixed an issue which caused empty rows to sometimes appear in the table of contents - [097197c](https://github.com/Kapeli/Dash-iOS/commit/097197c828db9e1b1524f46da41a0db92e7376cf)
* Fixed an iPad-only crash which occurred in Settings while pressing Done with the search field active - [e87a069](https://github.com/Kapeli/Dash-iOS/commit/e87a069b6a94f31d9fac91be9ac6ca4569bcf251)
* Fixed an issue in the User Contributed repo which caused author names to not be truncated when there's not enough space - [3482d8f](https://github.com/Kapeli/Dash-iOS/commit/3482d8f7cd0f6e19b1a42c80a69f09783565522a)
* Added "Data Source" type - [6a45537](https://github.com/Kapeli/Dash-iOS/commit/6a45537447319a68341c2b4686da3b4753828310)
# 1.6.1
* Added support for receiving docsets using AirDrop. Thanks to [@vinayjn](https://github.com/vinayjn) for the great work on this - [#36](https://github.com/Kapeli/Dash-iOS/pull/36)
* Added Pug docset. Removed Jade docset - [36fdff3](https://github.com/Kapeli/Dash-iOS/commit/36fdff3a2ac6d74bddb07ef8c430d46b19dd64d3)
* Fixed build products path. Thanks to [@RegalMedia](https://github.com/RegalMedia) for reporting the issue - [#28](https://github.com/Kapeli/Dash-iOS/issues/28)
* Fixed the display of included modules for Ruby docsets - [4416ccb](https://github.com/Kapeli/Dash-iOS/commit/4416ccbb7b78b0b4b0e72608f1ce5bd38a013b72)
* Fixed Dash App Store display/product name - [5015177](https://github.com/Kapeli/Dash-iOS/commit/5015177c23cefaea0688db95b462b33705e12952)
# 1.6.0
* Added support for cheat sheets - [#22](https://github.com/Kapeli/Dash-iOS/pull/22)
* Added support for user contributed docsets - [#20](https://github.com/Kapeli/Dash-iOS/pull/20)
* Added state restoration support. Thanks to [@zhongwuzw](https://github.com/zhongwuzw) for the great work on this - [#18](https://github.com/Kapeli/Dash-iOS/pull/18)
* Fixed Unity 3D docset bug which caused it to not remember the selected language. Thanks to [@hantengx](https://github.com/hantengx) for reporting the issue - [#17](https://github.com/Kapeli/Dash-iOS/issues/17)
* Fixed a crash in the docset downloader. Thanks to [@zhongwuzw](https://github.com/zhongwuzw) for the fix - [#16](https://github.com/Kapeli/Dash-iOS/pull/16)
* Stopped an evil `if()` from taking over the world. Thanks to [@BalestraPatrick](https://github.com/BalestraPatrick) for reporting the issue and [@flovilmart](https://github.com/flovilmart) for fixing it - [#4](https://github.com/Kapeli/Dash-iOS/pull/4)
================================================
FILE: Dash/Base.lproj/Main.storyboard
================================================
================================================
FILE: Dash/DHAppDelegate.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHWindow.h"
@interface DHAppDelegate : UIResponder
@property (strong, nonatomic) DHWindow *_window;
@property (strong) NSTimer *clipboardChangedTimer;
+ (DHAppDelegate *)sharedDelegate;
+ (UIStoryboard *)mainStoryboard;
@end
================================================
FILE: Dash/DHAppDelegate.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHAppDelegate.h"
#import "DHDocsetDownloader.h"
#import "DHUserRepo.h"
#import "DHCheatRepo.h"
#import "DHDocsetTransferrer.h"
#import "DHDocsetManager.h"
#import "DHTarixProtocol.h"
#import "DHBlockProtocol.h"
#import "DHAppleAPIProtocol.h"
#import "DHCSS.h"
#import "DHWebViewController.h"
#import "DHAppUpdateChecker.h"
#import "DHDocsetBrowser.h"
#ifdef APP_STORE
#import
#endif
#import "DHRemoteServer.h"
#import "DHRemoteProtocol.h"
@implementation DHAppDelegate
+ (DHAppDelegate *)sharedDelegate
{
return (id)[[UIApplication sharedApplication] delegate];
}
+ (UIStoryboard *)mainStoryboard
{
return [self sharedDelegate].window.rootViewController.storyboard;
}
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self setDoNotBackUp]; // this needs to be first because it deletes the preferences after a backup restore
NSLog(@"Home Path: %@", homePath);
[self.window makeKeyAndVisible];
NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
if(cacheDir)
{
[[NSFileManager defaultManager] removeItemAtPath:[cacheDir stringByAppendingPathComponent:@"com.apple.nsurlsessiond/Downloads"] error:nil];
}
#ifdef APP_STORE
#ifndef DEBUG
[[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"40091a11e4b749fcb7808992057b165a"];
[[BITHockeyManager sharedHockeyManager].crashManager setCrashManagerStatus:BITCrashManagerStatusAutoSend];
[[BITHockeyManager sharedHockeyManager] startManager];
[[BITHockeyManager sharedHockeyManager].authenticator authenticateInstallation];
#endif
#endif
#ifdef DEBUG
[self checkCommitHashes];
#endif
// NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"Mozilla/5.0 (iPhone; CPU iPhone OS 10_10 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B411 Xcode/6.1.0", @"UserAgent", nil];
// [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
// NSLog(@"%@", [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]);
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:4*1024*1024 diskCapacity:32*1024*1024 diskPath:@"dh_nsurlcache"];
[sharedCache removeAllCachedResponses];
[NSURLCache setSharedURLCache:sharedCache];
[NSURLProtocol registerClass:[DHTarixProtocol class]];
[NSURLProtocol registerClass:[DHAppleAPIProtocol class]];
[NSURLProtocol registerClass:[DHRemoteProtocol class]];
[NSURLProtocol registerClass:[DHBlockProtocol class]];
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"Defaults" ofType:@"plist"]]];
[DHDocset stepLock];
[DHDocsetManager sharedManager];
[DHCSS sharedCSS];
[DHDBResultSorter sharedSorter];
[DHDBNestedResultSorter sharedSorter];
// self.window.tintColor = [UIColor purpleColor];
[DHDocsetDownloader sharedDownloader];
[DHDocsetTransferrer sharedTransferrer];
[DHUserRepo sharedUserRepo];
[DHCheatRepo sharedCheatRepo];
[DHRemoteServer sharedServer];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clipboardChanged:) name:UIPasteboardChangedNotification object:nil];
return YES;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UITextField *lagFreeField = [[UITextField alloc] init];
[self.window addSubview:lagFreeField];
[lagFreeField becomeFirstResponder];
[lagFreeField resignFirstResponder];
[lagFreeField setHidden:YES];
return YES;
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)actualURL sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
if([[actualURL absoluteString] hasCaseInsensitivePrefix:@"dash://"] || [[actualURL absoluteString] hasCaseInsensitivePrefix:@"dash-plugin://"])
{
[[NSNotificationCenter defaultCenter] postNotificationName:DHPrepareForURLSearch object:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:DHPerformURLSearch object:[actualURL absoluteString]];
});
}
else if([[actualURL pathExtension] isCaseInsensitiveEqual:@"docset"])
{
NSError *error;
NSString *fileName = [actualURL lastPathComponent];
NSURL *copyToURL = [[NSURL fileURLWithPath:transfersPath] URLByAppendingPathComponent:fileName isDirectory:NO];
[[NSFileManager defaultManager] removeItemAtPath:copyToURL.path error:nil];
[[NSFileManager defaultManager] moveItemAtURL:actualURL toURL:copyToURL error:&error];
NSString *title;
NSString *message;
if(error)
{
title = @"Import Failed";
message = @"Could not import the docset. Please try again!";
NSLog(@"%@", error.localizedDescription);
}
else
{
title = @"Import Successful";
message = @"You can find the docset in Settings, under the Transfer Docsets section.";
NSLog(@"Docset successfully imported");
}
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle: UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleDefault handler:nil]];
[[self topViewController] presentViewController:alert animated:YES completion:nil];
}
return YES;
}
- (UINavigationController *)navigationController
{
if([self.window.rootViewController isKindOfClass:[UINavigationController class]])
{
return (UINavigationController*)self.window.rootViewController;
}
return self.window.rootViewController.navigationController;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
if(![[DHAppUpdateChecker sharedUpdateChecker] alertIfUpdatesAreScheduled])
{
[[DHAppUpdateChecker sharedUpdateChecker] backgroundCheckForUpdatesIfNeeded];
if(![[DHDocsetDownloader sharedDownloader] alertIfUpdatesAreScheduled])
{
[[DHDocsetDownloader sharedDownloader] backgroundCheckForUpdatesIfNeeded];
if(![[DHUserRepo sharedUserRepo] alertIfUpdatesAreScheduled])
{
[[DHUserRepo sharedUserRepo] backgroundCheckForUpdatesIfNeeded];
if(![[DHCheatRepo sharedCheatRepo] alertIfUpdatesAreScheduled])
{
[[DHCheatRepo sharedCheatRepo] backgroundCheckForUpdatesIfNeeded];
}
}
}
}
}
- (void)applicationWillTerminate:(UIApplication *)application
{
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
[[NSURLCache sharedURLCache] removeAllCachedResponses];
NSLog(@"did receive memory warning");
}
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completionHandler();
}];
}
#pragma mark - UIStateRestoration
- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder {
return YES;
}
- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder {
return YES;
}
- (void)setDoNotBackUp
{
NSString *path = [homePath stringByAppendingPathComponent:@"Docsets"];
NSFileManager *fileManager = [NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:path])
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
for(NSString *key in @[@"DHDocsetDownloaderScheduledUpdate", @"DHDocsetDownloader", @"DHDocsetTransferrer", @"docsets"])
{
[defaults removeObjectForKey:key];
}
[fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
NSURL *url = [NSURL fileURLWithPath:path];
[url setResourceValue:@YES forKey: NSURLIsExcludedFromBackupKey error:nil];
}
- (void)clipboardChanged:(NSNotification*)notification
{
NSString *string = [UIPasteboard generalPasteboard].string;
if(string && string.length && [DHRemoteServer sharedServer].connectedRemote)
{
self.clipboardChangedTimer = [self.clipboardChangedTimer invalidateTimer];
self.clipboardChangedTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 block:^{
[[DHRemoteServer sharedServer] sendObject:@{@"string": string} forRequestName:@"syncClipboard" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
} repeats:NO];
}
}
- (void)checkCommitHashes
{
NSDictionary *hashes = @{@"DHDBSearcher": @"31900da1",
@"DHDBResult": @"31900da1",
@"DHDBUnifiedResult": @"31900da1",
@"DHQueuedDB": @"31900da1",
@"DHUnifiedQueuedDB": @"31900da1",
@"DHDBUnifiedOperation": @"31900da1",
@"DHWebViewController": @"da4e9df6",
@"DHWebPreferences": @"6896e837",
@"DHDocsetDownloader": @"3efbf5e6",
@"PlatformIcons": @"31900da1",
@"DHTypes": @"062c6b03",
@"Types": @"062c6b03",
@"CSS": @"7be5591d",
};
[hashes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSString *plistHash = [[NSBundle mainBundle] infoDictionary][[key stringByAppendingString:@"Commit"]];
if(![plistHash isEqualToString:@"not set"] && ![plistHash isEqualToString:obj])
{
NSLog(@"Wrong git hash %@ for %@. Maybe you forgot to sync something or update this list?", plistHash, key);
}
}];
}
- (DHWindow *)window
{
if(self._window)
{
return self._window;
}
self._window = [[DHWindow alloc] init];
return self._window;
}
- (UIViewController *)topViewController
{
return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
}
- (UIViewController *)topViewController:(UIViewController *)rootViewController
{
if (rootViewController.presentedViewController == nil)
{
return rootViewController;
}
if ([rootViewController.presentedViewController isKindOfClass:[UINavigationController class]])
{
UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController;
UIViewController *lastViewController = [[navigationController viewControllers] lastObject];
return [self topViewController:lastViewController];
}
UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController;
return [self topViewController:presentedViewController];
}
@end
================================================
FILE: Dash/DHAppUpdateChecker.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHAppUpdateChecker : NSObject
+ (DHAppUpdateChecker *)sharedUpdateChecker;
- (BOOL)alertIfUpdatesAreScheduled;
- (void)backgroundCheckForUpdatesIfNeeded;
@end
================================================
FILE: Dash/DHAppUpdateChecker.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHAppUpdateChecker.h"
#define DHAppUpdateCheckerLastCheckDate @"DHAppUpdateCheckerLastCheckDate"
#define DHAppUpdateCheckerScheduledUpdateVersion @"DHAppUpdateCheckerScheduledUpdateVersion"
@implementation DHAppUpdateChecker
+ (DHAppUpdateChecker *)sharedUpdateChecker
{
#ifdef APP_STORE
return nil;
#endif
static dispatch_once_t pred;
static DHAppUpdateChecker *_checker = nil;
dispatch_once(&pred, ^{
_checker = [[DHAppUpdateChecker alloc] init];
});
return _checker;
}
- (void)backgroundCheckForUpdatesIfNeeded
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDate *lastDate = [defaults objectForKey:DHAppUpdateCheckerLastCheckDate];
if(!lastDate || [[NSDate date] timeIntervalSinceDate:lastDate] > 60*60*24)
{
[defaults setObject:[NSDate date] forKey:DHAppUpdateCheckerLastCheckDate];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
NSData *jsonData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:[@"https://kapeli.com/dash_ios.json?bundle_id=" stringByAppendingString:[NSBundle mainBundle].bundleIdentifier]]];
if(jsonData)
{
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
if(json)
{
NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];
NSString *myVersion = [infoDict objectForKey:@"CFBundleVersion"];
if([myVersion integerValue] < [json[@"version"] integerValue])
{
[[NSUserDefaults standardUserDefaults] setObject:json[@"version"] forKey:DHAppUpdateCheckerScheduledUpdateVersion];
}
}
}
});
}
}
- (BOOL)alertIfUpdatesAreScheduled
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *version = [defaults objectForKey:DHAppUpdateCheckerScheduledUpdateVersion];
if(version)
{
[defaults removeObjectForKey:DHAppUpdateCheckerScheduledUpdateVersion];
NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];
NSString *myVersion = [infoDict objectForKey:@"CFBundleVersion"];
if([version integerValue] > [myVersion integerValue])
{
[UIAlertView showWithTitle:@"Dash Update Available" message:@"A new version of Dash has been released. Would you like to update?" cancelButtonTitle:@"Maybe Later" otherButtonTitles:@[@"Update"] tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
if(buttonIndex == alertView.firstOtherButtonIndex)
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://kapeli.com/dash_ios?ref=update#update"]];
}
}];
return YES;
}
}
return NO;
}
@end
================================================
FILE: Dash/DHAppleAPIProtocol.h
================================================
//
// Copyright (C) 2018 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHAppleAPIProtocol : NSURLProtocol
@end
================================================
FILE: Dash/DHAppleAPIProtocol.m
================================================
//
// Copyright (C) 2018 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHAppleAPIProtocol.h"
#import
#import "DHDocsetManager.h"
#import "DHLatencyTester.h"
#import "DHWebViewController.h"
#import "DHTocBrowser.h"
@implementation DHAppleAPIProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if([[[request URL] scheme] isCaseInsensitiveEqual:@"dash-apple-api"])
{
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (void)startLoading
{
@autoreleasepool {
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[self.request URL] MIMEType:@"text/html" expectedContentLength:-1 textEncodingName:nil];
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
NSString *url = [[self.request.URL absoluteString] substringToString:@"#"];
if(![url contains:@"&language="])
{
url = [url stringByAppendingFormat:@"&language=%@", ([DHAppleActiveLanguage currentLanguage] == DHNewActiveAppleLanguageObjC) ? @"occ" : @"swift"];
}
DHDocset *docset = [[DHDocsetManager sharedManager] appleAPIReferenceDocset];
NSString *toolPath = [docset.documentsPath stringByAppendingPathComponent:@"Apple Docs Helper"];
NSData *data = [@"ErrorError. Please reinstall the Apple API Reference docset." dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
if(toolPath.length)
{
@try {
DHViewer *viewer = [DHViewer sharedViewer];
DHCommandLineParser *parser = [DHCommandLineParser sharedParser];
parser.ownPath = toolPath;
NSString *bestMirror = [[DHLatencyTester sharedLatency].bestMirror stringByConvertingKapeliHttpURLToHttps];
bestMirror = (bestMirror) ? bestMirror : @"https://kapeli.com/feeds/";
viewer.isIOS = YES;
parser.bestMirror = bestMirror;
viewer.url = url;
parser.dashBuildNumber = 450;
data = [[viewer htmlOutput] dataUsingEncoding:NSUTF8StringEncoding];
[self setUpTOC:viewer];
[DHXcodeHelper cleanUp];
[DHViewer cleanUp];
}
@catch(NSException *exception) { NSLog(@"%@ %@", exception, [exception callStackSymbols]); }
}
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
}
}
- (void)setUpTOC:(DHViewer *)viewer
{
dispatch_sync(dispatch_get_main_queue(), ^{
DHWebViewController *controller = [DHWebViewController sharedWebViewController];
if(iPad && isRegularHorizontalClass)
{
if(controller.methodsPopover.popoverVisible)
{
[controller.methodsPopover dismissPopoverAnimated:YES];
}
}
else
{
[[controller.actualTOCBrowser searchDisplayController] setActive:NO animated:NO];
[[controller.actualTOCBrowser presentingViewController] dismissViewControllerAnimated:YES completion:nil];
}
controller.lastTocBrowser = nil;
controller.currentMethods = viewer.tocEntries.count ? viewer.tocEntries : nil;
controller.navigationItem.rightBarButtonItem = (viewer.tocEntries.count) ? [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"tocMenu"] style:UIBarButtonItemStylePlain target:controller action:@selector(tocButtonPressed:)] : nil;
});
}
- (void)stopLoading
{
}
@end
================================================
FILE: Dash/DHAppleActiveLanguage.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHAppleActiveLanguage : NSObject
@property (assign) NSInteger activeLanguage;
+ (NSInteger)currentLanguage;
+ (void)setLanguage:(NSInteger)language;
@end
#define DHNewAppleActiveLanguageKey @"DHNewAppleActiveLanguageKey"
#define DHNewActiveAppleLanguageSwift 0
#define DHNewActiveAppleLanguageObjC 1
================================================
FILE: Dash/DHAppleActiveLanguage.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHAppleActiveLanguage.h"
@implementation DHAppleActiveLanguage
+ (DHAppleActiveLanguage *)sharedActiveLanguage
{
static dispatch_once_t pred;
static DHAppleActiveLanguage *_singleton = nil;
dispatch_once(&pred, ^{
_singleton = [[DHAppleActiveLanguage alloc] init];
[_singleton setUp];
});
return _singleton;
}
- (void)setUp
{
self.activeLanguage = [[NSUserDefaults standardUserDefaults] integerForKey:DHNewAppleActiveLanguageKey];
}
+ (NSInteger)currentLanguage
{
return [[DHAppleActiveLanguage sharedActiveLanguage] activeLanguage];
}
+ (void)setLanguage:(NSInteger)language
{
[[NSUserDefaults standardUserDefaults] setInteger:language forKey:DHNewAppleActiveLanguageKey];
[DHAppleActiveLanguage sharedActiveLanguage].activeLanguage = language;
}
@end
================================================
FILE: Dash/DHBlockProtocol.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHBlockProtocol : NSURLProtocol
@end
================================================
FILE: Dash/DHBlockProtocol.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHBlockProtocol.h"
#import "DHWebViewController.h"
#import "DHDocsetManager.h"
#import "DHRemoteProtocol.h"
@implementation DHBlockProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
#ifdef DEBUG
if([[[request URL] scheme] isCaseInsensitiveEqual:@"http"])
{
NSLog(@"HTTP URL load detected: %@", request);
}
#endif
DHWebViewController *webController = [DHWebViewController sharedWebViewController];
NSString *url = [[request URL] absoluteString];
NSString *mainFrameURL = webController.mainFrameURL;
if([url isEqualToString:@"about:blank"])
{
return YES;
}
if([url contains:@"kapeli.com/"])
{
return NO;
}
if([[[request URL] scheme] hasCaseInsensitivePrefix:@"http"])
{
if(![[[request URL] host] length] || [[[request URL] host] hasSuffix:@"."])
{
return YES;
}
}
NSString *lastPathComponent = [url lastPathComponent];
if([lastPathComponent contains:@"xcode"] && [[lastPathComponent pathExtension] isCaseInsensitiveEqual:@"css"])
{
return YES;
}
if([webController isLocalURL] || ([mainFrameURL contains:@"developer.apple.com"] && [mainFrameURL contains:@"/documentation/"]))
{
if([url rangeOfString:@"disqus.com/" options:NSCaseInsensitiveSearch].location != NSNotFound || [url contains:@"lloogg.com/"] || [url rangeOfString:@"google-analytics.com/" options:NSCaseInsensitiveSearch].location != NSNotFound || [url contains:@"adzerk.net"] || [url contains:@"ghbtns.com"] || [url contains:@"platform.twitter.com/widgets.js"] || [url contains:@"analytics.twitter.com"] || [url contains:@"login.persona.org"] || [url contains:@"omtrdc.net"] || [url contains:@"google.com/cse/"] || [url contains:@"jashkenas.s3.amazonaws.com/images/a_documentcloud_project.png"] || [url contains:@"media.mongodb.org"] || [url contains:@"googleusercontent.com/beacon"] || [url contains:@"carbonads.com/"] || [url contains:@"facebook.com/plugins"] || [url contains:@"fbcdn.net"] || [url contains:@"http://nodejs.org/images/platform-icons.png"] || [url contains:@"apis.google.com/js/plusone.js"] || ([url contains:@"community.adobe.com"] && [url hasCaseInsensitiveSuffix:@".css"]))
{
return YES;
}
if(([url contains:@"codeclimate.com"] || [url contains:@"coveralls.io"] || [url contains:@"travis-ci.org"]) && [mainFrameURL rangeOfString:@"/Ruby%20DocSets/"].location == NSNotFound && [mainFrameURL rangeOfString:@"/Cocoa%20DocSets/"].location == NSNotFound && [mainFrameURL rangeOfString:@"/Cocoa%20DocSets/"].location == NSNotFound && [mainFrameURL rangeOfString:@"/Hex%20DocSets/"].location == NSNotFound && ([mainFrameURL rangeOfString:@"/Versioned%20DocSets/"].location == NSNotFound || [mainFrameURL rangeOfString:@"DHDocsetDownloader/"].location != NSNotFound) && ![url hasPrefix:@"file://"])
{
return YES;
}
BOOL isJSEnabled = NO;
BOOL blockOnlineResources = NO;
NSString *platform = nil;
BOOL isHTTPRequest = ([url hasCaseInsensitivePrefix:@"http"]);
BOOL isJSRequest = [[[url stringByDeletingPathFragment] pathExtension] rangeOfString:@"js"].location != NSNotFound;
if(isHTTPRequest || isJSRequest)
{
if([DHRemoteServer sharedServer].connectedRemote)
{
NSDictionary *userInfo = [DHRemoteProtocol lastResponseUserInfo];
platform = userInfo[@"platform"];
isJSEnabled = [userInfo[@"javaScriptEnabled"] boolValue];
blockOnlineResources = [userInfo[@"blocksOnline"] boolValue];
}
else
{
DHDocset *docset = [[DHDocsetManager sharedManager] docsetForDocumentationPage:mainFrameURL];
platform = docset.platform;
isJSEnabled = docset.isJavaScriptEnabled;
blockOnlineResources = docset.blocksOnlineResources;
}
}
if(([mainFrameURL containsAny:@[@"SciPy.docset", @"NumPy.docset"]]) && isHTTPRequest)
{
if(![url contains:@"mathjax"])
{
return YES;
}
}
if(([mainFrameURL containsAny:@[@"SQLAlchemy.docset", @"RequireJS.docset", @"Tornado.docset"]] || blockOnlineResources) && isHTTPRequest)
{
if([mainFrameURL containsAny:@[@"Julia.docset"]] && [url contains:@"mathjax"])
{
return NO;
}
return YES;
}
if([mainFrameURL contains:@"jQuery%20UI.docset"] || [mainFrameURL contains:@"jQuery%20Mobile.docset"] || [mainFrameURL contains:@"jQuery.docset"] || [mainFrameURL contains:@"Foundation.docset"])
{
return NO;
}
if(isJSRequest)
{
if([url contains:@"Sass.docset"] || [url rangeOfString:@"UnderscoreJS.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"BackboneJS.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"Bootstrap.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url contains:@"CSS.docset"] || [url contains:@"HTML.docset"] || [url contains:@"XSLT.docset"] || [url contains:@"XUL.docset"] || [url contains:@"SVG.docset"] || [url contains:@"JavaScript.docset"])
{
return NO;
}
if([url contains:@"Dojo.docset"] || [url contains:@"Elixir.docset"] || [url contains:@"KnockoutJS.docset"] || [url contains:@"PhoneGap.docset"] || [url contains:@"MarionetteJS.docset"])
{
return NO;
}
if([url contains:@"Bourbon.docset"] || [url contains:@"Puppet.docset"] || [url contains:@"Neat.docset"] || [url contains:@"Xojo.docset"] || [url contains:@"Redis.docset"] || [url contains:@"sproutcore.docset"])
{
return NO;
}
if([url rangeOfString:@"Compass.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url contains:@"Processing.docset"])
{
return NO;
}
if([url contains:@"Laravel.docset"])
{
return NO;
}
if([url contains:@"Sencha%20Touch.docset"] || [url contains:@"ExtJS.docset"] || [url contains:@"Appcelerator%20Titanium.docset"])
{
return NO;
}
if([url rangeOfString:@"Zend_Framework.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([mainFrameURL contains:@"AngularJS.docset"] || [url rangeOfString:@"PrototypeJS.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"Go.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"RubyMotion.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"Ruby.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"Ruby%20on%20Rails.docset" options:NSCaseInsensitiveSearch].location != NSNotFound && ![[url lastPathComponent] isEqualToString:@"main.js"])
{
return NO;
}
if([url rangeOfString:@"Cappuccino.docset" options:NSCaseInsensitiveSearch].location != NSNotFound && ([url hasSuffix:@"jquery.js"] || [url hasSuffix:@"dynsections.js"]))
{
return NO;
}
if([url rangeOfString:@"Unity%203D.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"CoffeeScript.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"Yii.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"Scala.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"Akka.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url contains:@"/dash_scaladoc/"])
{
return NO;
}
if([url rangeOfString:@"YUI.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"Haskell.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"Android.docset" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return NO;
}
if([url rangeOfString:@"Drupal.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"CodeIgniter.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"Joomla.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"Symfony.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"TYPO3.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"Cocos2D-X.docset" options:NSCaseInsensitiveSearch].location != NSNotFound || [url rangeOfString:@"Zend_Framework.docset"].location != NSNotFound)
{
return NO;
}
if(isJSEnabled)
{
return NO;
}
if([platform isEqualToString:@"java"] || [platform isEqualToString:@"playjava"] || [platform isEqualToString:@"groovy"] || [platform isEqualToString:@"javafx"] || [platform isEqualToString:@"scaladoc"])
{
return NO;
}
if([[request mainDocumentURL] isEqual:[request URL]])
{
return NO;
}
return YES;
}
if([url rangeOfString:@"://developer.mozilla.org"].location != NSNotFound || [url rangeOfString:@"google.com/jsapi"].location != NSNotFound)
{
return YES;
}
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (void)startLoading
{
NSString *path = [[self.request URL] path];
NSString *extension = [path pathExtension];
NSString *mimeType = [NSString mimeTypeForPathExtension:extension];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[self.request URL]
MIMEType:mimeType
expectedContentLength:-1
textEncodingName:nil];
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
NSData *data = [@"" dataUsingEncoding:NSUTF8StringEncoding];
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
}
- (void)stopLoading
{
}
@end
================================================
FILE: Dash/DHBrowserCell.xib
================================================
================================================
FILE: Dash/DHBrowserTableView.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHBrowserTableView : UITableView
@end
@protocol DHBrowserTableViewDelegate
@optional
- (void)tableViewWillBeginEditing:(UITableView *)tableView;
- (void)tableViewDidBeginEditing:(UITableView *)tableView;
- (void)tableViewWillEndEditing:(UITableView *)tableView;
- (void)tableViewDidEndEditing:(UITableView *)tableView;
@end
================================================
FILE: Dash/DHBrowserTableView.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHBrowserTableView.h"
@implementation DHBrowserTableView
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
if(editing == self.editing)
{
return;
}
if(editing && [[self delegate] respondsToSelector:@selector(tableViewWillBeginEditing:)])
{
[(id)[self delegate] tableViewWillBeginEditing:self];
}
if(!editing && [[self delegate] respondsToSelector:@selector(tableViewWillEndEditing:)])
{
[(id)[self delegate] tableViewWillEndEditing:self];
}
[super setEditing:editing animated:animated];
if(editing && [[self delegate] respondsToSelector:@selector(tableViewDidBeginEditing:)])
{
[(id)[self delegate] tableViewDidBeginEditing:self];
}
if(!editing && [[self delegate] respondsToSelector:@selector(tableViewDidEndEditing:)])
{
[(id)[self delegate] tableViewDidEndEditing:self];
}
}
@end
================================================
FILE: Dash/DHBrowserTableViewCell.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHRightDetailLabel.h"
@interface DHBrowserTableViewCell : UITableViewCell
@property (assign) UIImageView *typeImageView;
@property (weak) IBOutlet DHRightDetailLabel *titleLabel;
@property (assign) IBOutlet UIImageView *platformImageView;
- (void)makeEntryCell;
@end
================================================
FILE: Dash/DHBrowserTableViewCell.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHBrowserTableViewCell.h"
@implementation DHBrowserTableViewCell
- (void)awakeFromNib
{
[super awakeFromNib];
self.titleLabel.isBrowserCell = YES;
}
- (void)makeEntryCell
{
if(self.typeImageView)
{
return;
}
self.separatorInset = UIEdgeInsetsMake(0, 70, 0, 0);
[self.titleLabel increaseFrameByX:24 y:0 width:-24 height:0];
UIImageView *typeImageView = [[UIImageView alloc] initWithFrame:CGRectMake(self.imageView.frame.origin.x+28, self.imageView.frame.origin.y+1, 14, 14)];
[typeImageView setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
self.typeImageView = typeImageView;
[self addSubview:typeImageView];
}
- (UIImageView *)imageView
{
return self.platformImageView;
}
- (UILabel *)textLabel
{
return self.titleLabel;
}
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index
{
if(self.editing)
{
return;
}
[super insertSubview:view atIndex:index];
}
- (NSString *)accessibilityValue
{
return [self.titleLabel accessibilityValue];
}
@end
================================================
FILE: Dash/DHCSS.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHCSS : NSObject
@property (strong) NSString *bothCSS;
@property (strong) NSString *swiftCSS;
@property (strong) NSString *objcCSS;
+ (DHCSS *)sharedCSS;
+ (NSString *)currentCSSString;
+ (NSString *)currentCSSStringWithTextModifier;
+ (int)activeAppleLanguage;
- (void)refreshActiveCSS;
- (void)modifyTextSize:(BOOL)increase;
- (BOOL)shouldModifyTextSize;
- (NSString *)textSizeAdjust;
@end
#define DHActiveAppleLanguageKey @"ActiveAppleLanguage"
#define DHActiveAppleLanguageBoth 0
#define DHActiveAppleLanguageSwift 1
#define DHActiveAppleLanguageObjC 2
================================================
FILE: Dash/DHCSS.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHCSS.h"
@implementation DHCSS
static int _activeAppleLanguage;
static long _textSizeAdjust;
+ (DHCSS *)sharedCSS
{
static dispatch_once_t pred;
static DHCSS *_css = nil;
dispatch_once(&pred, ^{
_css = [[DHCSS alloc] init];
[_css setUp];
});
return _css;
}
- (void)setUp
{
self.bothCSS = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"style" ofType:@"css"] encoding:NSUTF8StringEncoding error:nil];
self.objcCSS = [self.bothCSS stringByAppendingString:@"body.jazz #bashful #language #obj_c + label, body.jazz #bashful #language #objective_c + label {background: rgba(0,136,204,1) !important;color: rgba(255,255,255,1) !important;border-radius: 2px !important;cursor: default !important;} body#reference.jazz:not(.swift) .Swift { display: none !important; } body#reference.jazz:not(.java_script) .code-sample .Swift:only-child {display:inline-block !important} body#reference.jazz:not(.swift) .height-container .z-module-import {display:none !important}"];
self.swiftCSS = [self.bothCSS stringByAppendingString:@"body.jazz #bashful #language #swift + label {background: rgba(0,136,204,1) !important;color: rgba(255,255,255,1) !important;border-radius: 2px !important;cursor: default !important;} body#reference.jazz:not(.java_script) .Objective-C { display: none !important; } body#reference.jazz:not(.java_script) .code-sample .Objective-C:only-child {display:inline-block !important} body#reference.jazz:not(.java_script) .obj-c-only .height-container { display:none !important; } body#reference.jazz:not(.java_script) .obj-c-only .task-group-term a { display:block !important; text-decoration: line-through !important; -webkit-text-stroke-color: rgba(163,21,21,1) !important; -webkit-text-stroke-width: .0000000001px !important; } body.jazz:not(.java_script) .obj-c-only .task-group-term::after {content: '(Not available in Swift)' !important; color:rgba(163,21,21,1) !important; font-size:.8em !important; margin-left:5px !important; } body.jazz p.para.Swift {display:block !important} body.jazz .Swift { display: inline-block !important; } body.jazz #metadata_table .Swift { display: block !important; } body.jazz .task-group-term a.Swift, body.jazz #jump_to .Swift { display: block !important; } body.jazz .declaration .Swift { display: block !important; }"];
self.bothCSS = [self.bothCSS stringByAppendingString:@"body.jazz #bashful #language #both + label {background: rgba(0,136,204,1) !important;color: rgba(255,255,255,1) !important;border-radius: 2px !important;cursor: default !important;}"];
[self refreshActiveCSS];
[self refreshTextSize];
}
- (void)refreshActiveCSS
{
NSString *active = [[NSUserDefaults standardUserDefaults] objectForKey:DHActiveAppleLanguageKey];
if([active isEqualToString:@"swift"])
{
_activeAppleLanguage = DHActiveAppleLanguageSwift;
}
else if([active isEqualToString:@"obj_c"] || [active isEqualToString:@"objective_c"])
{
_activeAppleLanguage = DHActiveAppleLanguageObjC;
}
else
{
_activeAppleLanguage = DHActiveAppleLanguageBoth;
}
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
+ (NSString *)currentCSSString
{
DHCSS *css = [DHCSS sharedCSS];
if(_activeAppleLanguage == DHActiveAppleLanguageBoth)
{
return css.bothCSS;
}
else if(_activeAppleLanguage == DHActiveAppleLanguageObjC)
{
return css.objcCSS;
}
else if(_activeAppleLanguage == DHActiveAppleLanguageSwift)
{
return css.swiftCSS;
}
return css.bothCSS;
}
+ (NSString *)currentCSSStringWithTextModifier
{
return [NSString stringWithFormat:@"%@%@", [DHCSS currentCSSString], ([[DHCSS sharedCSS] shouldModifyTextSize]) ? [NSString stringWithFormat:@"\n\nbody {-webkit-text-size-adjust: %@}", [[DHCSS sharedCSS] textSizeAdjust]] : @""];
}
+ (int)activeAppleLanguage
{
return _activeAppleLanguage;
}
- (void)refreshTextSize
{
_textSizeAdjust = [[NSUserDefaults standardUserDefaults] integerForKey:@"DHTextSizeAdjust"];
}
- (NSString *)textSizeAdjust
{
return [NSString stringWithFormat:@"%ld%%", 100+_textSizeAdjust];
}
- (BOOL)shouldModifyTextSize
{
return _textSizeAdjust != 0;
}
- (void)modifyTextSize:(BOOL)increase
{
if(increase)
{
_textSizeAdjust += 5;
}
else
{
_textSizeAdjust -= 5;
}
if(_textSizeAdjust < -70)
{
_textSizeAdjust = -70;
}
else if(_textSizeAdjust > 70)
{
_textSizeAdjust = 70;
}
[[NSURLCache sharedURLCache] removeAllCachedResponses];
NSLog(@"Text size adjust set to %ld", _textSizeAdjust);
[[NSUserDefaults standardUserDefaults] setInteger:_textSizeAdjust forKey:@"DHTextSizeAdjust"];
}
@end
================================================
FILE: Dash/DHCheatRepo.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRepo.h"
@interface DHCheatRepo : DHRepo
@property (strong) NSDate *lastListLoad;
+ (instancetype)sharedCheatRepo;
@end
================================================
FILE: Dash/DHCheatRepo.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHCheatRepo.h"
#import "DHCheatRepoList.h"
@implementation DHCheatRepo
static id singleton = nil;
+ (instancetype)sharedCheatRepo
{
if(singleton)
{
return singleton;
}
id cheatRepo = [[DHAppDelegate mainStoryboard] instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
[cheatRepo setUp];
return cheatRepo;
}
- (void)setUp
{
[super setUp];
[self reloadCheatsheetsIfNeeded];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self reloadCheatsheetsIfNeeded];
}
- (void)reloadCheatsheetsIfNeeded
{
if(!self.loading && (!self.lastListLoad || (!self.searchBar.text.length && [[NSDate date] timeIntervalSinceDate:self.lastListLoad] > 300)))
{
self.loading = YES;
BOOL shouldDelay = [self.loadingText contains:@"Retrying"];
self.loadingText = nil;
self.searchBar.userInteractionEnabled = NO;
self.searchBar.alpha = 0.5;
self.searchBar.placeholder = @"Loading...";
[self.tableView reloadData];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
if(shouldDelay)
{
[NSThread sleepForTimeInterval:1.0];
}
[[DHCheatRepoList sharedCheatRepoList] reload];
NSMutableArray *feeds = [[DHCheatRepoList sharedCheatRepoList] allCheatsheets];
dispatch_sync(dispatch_get_main_queue(), ^{
if(feeds.count)
{
NSArray *savedFeeds = [[NSUserDefaults standardUserDefaults] objectForKey:[self defaultsKey]];
for(NSDictionary *feedDictionary in savedFeeds)
{
DHFeed *savedFeed = [DHFeed feedWithDictionaryRepresentation:feedDictionary];
NSUInteger index = [feeds indexOfObject:savedFeed];
if(index != NSNotFound)
{
DHFeed *feed = feeds[index];
feed.installed = savedFeed.installed;
feed.installedVersion = savedFeed.installedVersion;
feed.size = savedFeed.size;
}
}
[feeds sortUsingFunction:compareFeeds context:nil];
self.lastListLoad = [NSDate date];
self.searchBar.userInteractionEnabled = YES;
self.searchBar.alpha = 1.0;
self.searchBar.placeholder = @"Find cheat sheets to download";
self.loading = NO;
self.feeds = feeds;
[self.tableView reloadData];
}
else
{
self.loadingText = @"Loading failed. Retrying...";
[self.tableView reloadData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.loading = NO;
[self reloadCheatsheetsIfNeeded];
});
}
});
});
}
}
- (NSString *)installFeed:(DHFeed *)feed isAnUpdate:(BOOL)isAnUpdate
{
NSObject *identifier = [[NSObject alloc] init];
feed.identifier = identifier;
feed.waiting = YES;
BOOL didStallOnce = NO;
BOOL didSetStallLabel = NO;
while([self shouldStall] && feed.installing && feed.identifier == identifier)
{
if(didStallOnce && !didSetStallLabel)
{
didSetStallLabel = YES;
dispatch_sync(dispatch_get_main_queue(), ^{
[feed setDetailString:@"Waiting..."];
[[feed cell].titleLabel setRightDetailText:@"Waiting..." adjustMainWidth:YES];
[feed setMaxRightDetailWidth:[feed cell].titleLabel.maxRightDetailWidth];
});
}
didStallOnce = YES;
[NSThread sleepForTimeInterval:1.0f];
}
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
feed.waiting = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *installPath = [self docsetPathForFeed:feed];
NSString *tempPath = [self uniqueTempDirAtPath:installPath];
NSString *tempFile = [tempPath stringByAppendingPathComponent:@"dash_temp_docset.tgz"];
NSString *tarixFile = [tempFile stringByAppendingString:@".tarix"];
__block BOOL shouldWait = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
shouldWait = [[DHLatencyTester sharedLatency] performTests:NO];
});
if(shouldWait)
{
[NSThread sleepForTimeInterval:3.0f];
}
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
NSString *error = nil;
DHFeedResult *feedResult = [self loadFeed:feed error:&error];
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
if(feedResult && feed.installing)
{
feed.feedResult = feedResult;
feedResult.feed = feed;
NSError *downloadError = nil;
if(feedResult.downloadURLs.count)
{
NSString *downloadURL = feedResult.downloadURLs[0];
[self emptyTrashAtPath:tempPath];
if(![fileManager createDirectoryAtPath:tempPath withIntermediateDirectories:YES attributes:nil error:nil])
{
return @"Couldn't create install directory.";
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
NSURL *url = [NSURL URLWithString:downloadURL];
if(url)
{
BOOL result = NO;
NSURL *tarixURL = [NSURL URLWithString:[[downloadURL stringByAppendingString:@".tarix"] stringByConvertingKapeliHttpURLToHttps]];
feedResult.hasTarix = [NSURL URLIsFound:[tarixURL absoluteString] timeoutInterval:120.0f checkForRedirect:YES];
downloadError = nil;
#ifdef DEBUG
NSLog(@"Downloading %@", url);
#endif
if([DHFileDownload downloadItemAtURL:url toFile:tempFile error:&downloadError delegate:self identifier:feedResult] && !downloadError)
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Waiting..."];
@synchronized([DHDocsetIndexer class])
{
if(!feedResult.hasTarix)
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[fileManager removeItemAtPath:tarixFile error:nil];
result = [DHUnarchiver unarchiveArchive:tempFile delegate:feedResult];
}
else
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Preparing..."];
result = [DHFileDownload downloadItemAtURL:tarixURL toFile:tarixFile error:nil delegate:nil identifier:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't download index file.";
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Extracting..."];
result = [DHUnarchiver unarchiveArchive:tarixFile delegate:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive index file.";
}
[fileManager removeItemAtPath:tarixFile error:nil];
tarixFile = [fileManager firstFileWithExtension:@"tarix" atPath:tempPath ignoreHidden:YES];
if(!tarixFile)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't find index file.";
}
tarixFile = [tempPath stringByAppendingPathComponent:tarixFile];
result = [DHUnarchiver unpackTarixDocset:tempFile tarixPath:tarixFile delegate:feedResult];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive docset.";
}
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive docset.";
}
if(!feedResult.hasTarix)
{
[fileManager removeItemAtPath:tempFile error:nil];
}
DHDocset *docset = [DHDocset firstDocsetInsideFolder:tempPath];
if(!docset)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't install docset.";
}
else if(feedResult.hasTarix)
{
[fileManager moveItemAtPath:tarixFile toPath:docset.tarixIndexPath error:nil];
[fileManager moveItemAtPath:tempFile toPath:docset.tarixPath error:nil];
}
[DHDocsetIndexer indexerForDocset:docset delegate:feedResult];
[fileManager removeItemAtPath:docset.sqlPath error:nil];
[fileManager removeItemAtPath:[docset.sqlPath stringByAppendingString:@"-shm"] error:nil];
[fileManager removeItemAtPath:[docset.sqlPath stringByAppendingString:@"-wal"] error:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:installPath];
NSString *file = nil;
while(file = [dirEnum nextObject])
{
[dirEnum skipDescendents];
NSString *filePath = [installPath stringByAppendingPathComponent:file];
if(![filePath isEqualToString:tempPath])
{
NSString *trashPath = [self uniqueTrashPath];
[fileManager moveItemAtPath:filePath toPath:trashPath error:nil];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[self emptyTrashAtPath:trashPath];
});
}
}
[fileManager moveItemAtPath:docset.path toPath:[installPath stringByAppendingPathComponent:[docset.path lastPathComponent]] error:nil];
[self emptyTrashAtPath:tempPath];
return nil;
}
}
else if(downloadError.code == DHDownloadCancelled)
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
else
{
[self emptyTrashAtPath:tempPath];
}
}
}
return @"Couldn't download cheat sheet.";
}
else
{
return error;
}
return nil;
}
- (DHFeedResult *)loadFeed:(DHFeed *)feed error:(NSString **)returnError
{
DHFeedResult *feedResult = [[DHFeedResult alloc] init];
DHCheatRepoList *list = [DHCheatRepoList sharedCheatRepoList];
feedResult.downloadURLs = @[[list downloadURLForEntry:feed]];
feedResult.version = [list versionForEntry:feed];
return feedResult;
}
- (NSString *)docsetInstallFolderName
{
return @"Cheat Sheets";
}
- (void)showUpdateRequestForFeeds:(NSArray *)toUpdate count:(NSInteger)feedCount docsetList:(NSString *)docsetList
{
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:[self defaultsScheduledUpdateKey]];
[UIAlertView showWithTitle:@"Updates Found" message:[NSString stringWithFormat:@"Updates are available for %ld %@:%@%@", (long)feedCount, (feedCount > 1) ? @"cheat sheets" : @"cheat sheet", (feedCount > 1) ? @"\n\n" : @" ", docsetList] cancelButtonTitle:@"Maybe Later" otherButtonTitles:@[@"Update"] tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
if(buttonIndex != alertView.cancelButtonIndex)
{
if(toUpdate)
{
[self updateFeeds:toUpdate];
}
else
{
[self checkForUpdatesAndShowInterface:NO updateWithoutAsking:YES];
}
}
}];
}
- (IBAction)errorButtonPressed:(id)sender
{
NSUInteger row = [sender tag];
DHFeed *feed = [self activeFeeds][row];
[[[UIAlertView alloc] initWithTitle:@"Cheat Sheet Install Failed" message:[NSString stringWithFormat:@"%@ Please check your Internet connection and available free space and try again.", feed.error] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
+ (id)alloc
{
if(singleton)
{
return singleton;
}
return [super alloc];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if(singleton)
{
return singleton;
}
self = [super initWithCoder:aDecoder];
singleton = self;
return self;
}
@end
================================================
FILE: Dash/DHCheatRepoList.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHFeed.h"
@interface DHCheatRepoList : NSObject
@property (retain) NSDictionary *json;
+ (DHCheatRepoList *)sharedCheatRepoList;
- (NSMutableArray *)allCheatsheets;
- (NSString *)downloadURLForEntry:(DHFeed *)entry;
- (NSString *)versionForEntry:(DHFeed *)entry;
- (void)reload;
@end
================================================
FILE: Dash/DHCheatRepoList.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHCheatRepoList.h"
#import "DHLatencyTester.h"
@implementation DHCheatRepoList
+ (DHCheatRepoList *)sharedCheatRepoList
{
static dispatch_once_t pred;
static DHCheatRepoList *_cheatList = nil;
dispatch_once(&pred, ^{
_cheatList = [[DHCheatRepoList alloc] init];
[_cheatList setUp];
});
return _cheatList;
}
- (void)setUp
{
}
- (void)reload
{
BOOL success = NO;
NSString *url = [[[[DHLatencyTester sharedLatency] bestMirror] stringByAppendingString:@"zzz/cheatsheets/cheat.json"] stringByConvertingKapeliHttpURLToHttps];
NSString *json = [NSString stringWithContentsOfURLString:url];
if(json)
{
NSDictionary *newJSON = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
if(newJSON)
{
self.json = newJSON;
success = YES;
}
}
if(!success)
{
dispatch_sync(dispatch_get_main_queue(), ^{
[[DHLatencyTester sharedLatency] performTests:YES];
});
}
}
- (NSMutableArray *)allCheatsheets
{
NSMutableArray *entries = [NSMutableArray array];
[self.json[@"cheatsheets"] enumerateKeysAndObjectsUsingBlock:^(id key, NSDictionary *value, BOOL *stop) {
DHFeed *entry = [DHFeed entryWithName:value[@"name"] platform:@"cheatsheet" icon:nil];
entry.aliases = value[@"aliases"];
entry._uniqueIdentifier = key;
entry._icon = [UIImage imageNamed:@"cheatsheet"];
[entries addObject:entry];
}];
if(!entries.count)
{
return nil;
}
return entries;
}
- (NSString *)versionForEntry:(DHFeed *)entry
{
if(!self.json)
{
return nil;
}
NSString *globalVersion = self.json[@"global_version"];
NSString *version = self.json[@"cheatsheets"][entry.uniqueIdentifier][@"version"];
if(version)
{
return [NSString stringWithFormat:@"Global: %@, Individual: %@", globalVersion, version];
}
return nil;
}
- (NSString *)downloadURLForEntry:(DHFeed *)entry
{
if(!self.json)
{
return nil;
}
return [[[[DHLatencyTester sharedLatency] bestMirror] stringByAppendingFormat:@"zzz/cheatsheets/%@.tgz", entry.uniqueIdentifier] stringByConvertingKapeliHttpURLToHttps];
}
@end
================================================
FILE: Dash/DHDBNestedResultSorter.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@class DHDBResult;
@interface DHDBNestedResultSorter : NSObject {
}
@property (retain) NSMutableDictionary *ranks;
+ (DHDBNestedResultSorter *)sharedSorter;
- (void)saveDefaults:(NSMutableDictionary *)entries;
- (DHDBResult *)sortNestedResults:(DHDBResult *)parentResult;
- (NSInteger)rankForResult:(DHDBResult *)aResult;
- (void)increaseRankForResult:(DHDBResult *)aResult;
@end
================================================
FILE: Dash/DHDBNestedResultSorter.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDBNestedResultSorter.h"
#import "DHDBResult.h"
@implementation DHDBNestedResultSorter
static DHDBNestedResultSorter *_sorter = nil;
+ (DHDBNestedResultSorter *)sharedSorter
{
@synchronized([DHDBNestedResultSorter class])
{
if(!_sorter)
{
_sorter = [[DHDBNestedResultSorter alloc] init];
[_sorter setUp];
}
}
return _sorter;
}
- (void)setUp
{
self.ranks = [[[NSUserDefaults standardUserDefaults] objectForKey:@"nestedResultSortRanks"] mutableCopy];
if(!self.ranks)
{
self.ranks = [NSMutableDictionary dictionary];
}
}
- (DHDBResult *)sortNestedResults:(DHDBResult *)parentResult
{
NSMutableArray *allResults = [parentResult similarResults];
[allResults insertObject:parentResult atIndex:0];
parentResult.similarResults = [NSMutableArray array];
[parentResult setIsActive:NO];
NSCharacterSet *symbolsSet = [NSCharacterSet characterSetWithCharactersInString:@":./\\#"];
[allResults sortUsingComparator:^NSComparisonResult(DHDBResult *obj1, DHDBResult *obj2) {
NSInteger rank1 = [self rankForResult:obj1];
NSInteger rank2 = [self rankForResult:obj2];
if(rank1 > rank2)
{
return NSOrderedAscending;
}
else if(rank2 > rank1)
{
return NSOrderedDescending;
}
if(obj1.perfectMatchOriginal && !obj2.perfectMatchOriginal)
{
return NSOrderedAscending;
}
else if(obj2.perfectMatchOriginal && !obj1.perfectMatchOriginal)
{
return NSOrderedDescending;
}
if(obj1.queryIsPrefixOfOriginal && !obj2.queryIsPrefixOfOriginal)
{
return NSOrderedAscending;
}
else if(obj2.queryIsPrefixOfOriginal && !obj1.queryIsPrefixOfOriginal)
{
return NSOrderedDescending;
}
if(obj1.queryIsSuffixOfOriginal && !obj2.queryIsSuffixOfOriginal)
{
return NSOrderedAscending;
}
else if(obj2.queryIsSuffixOfOriginal && !obj1.queryIsSuffixOfOriginal)
{
return NSOrderedDescending;
}
if(obj1.originalMatchesQueryAtAll && !obj2.originalMatchesQueryAtAll)
{
return NSOrderedAscending;
}
else if(obj2.originalMatchesQueryAtAll && !obj1.originalMatchesQueryAtAll)
{
return NSOrderedDescending;
}
NSInteger count1 = [[[obj1 declaredInPage] stringByReplacingOccurrencesOfString:@"..." withString:@""] countOfCharactersInSet:symbolsSet];
NSInteger count2 = [[[obj2 declaredInPage] stringByReplacingOccurrencesOfString:@"..." withString:@""] countOfCharactersInSet:symbolsSet];
if(count1 < count2)
{
return NSOrderedAscending;
}
else if(count2 < count1)
{
return NSOrderedDescending;
}
return [[obj1 declaredInPage] localizedCaseInsensitiveCompare:[obj2 declaredInPage]];
}];
DHDBResult *newParent = allResults[0];
[allResults removeObjectAtIndex:0];
newParent.similarResults = allResults;
[newParent setIsActive:YES];
return newParent;
}
- (NSInteger)rankForResult:(DHDBResult *)aResult
{
NSMutableDictionary *nestedEntry = (self.ranks)[[aResult name]];
if(!nestedEntry)
{
return 0;
}
NSNumber *rank = nestedEntry[[aResult relativePath]];
if(!rank)
{
return 0;
}
return [rank integerValue];
}
- (void)increaseRankForResult:(DHDBResult *)aResult
{
if(aResult.isRemote)
{
return;
}
NSMutableDictionary *entries = self.ranks;
if(entries.count > 500)
{
[self purgeDictionary:entries];
}
NSMutableDictionary *nestedEntry = entries[[aResult name]];
if(!nestedEntry)
{
nestedEntry = [NSMutableDictionary dictionaryWithObject:@1 forKey:[aResult relativePath]];
entries[[aResult name]] = nestedEntry;
}
else
{
nestedEntry = [NSMutableDictionary dictionaryWithDictionary:nestedEntry];
NSNumber *rank = nestedEntry[[aResult relativePath]];
if([rank integerValue] <= 0)
{
nestedEntry[[aResult relativePath]] = @1;
}
else
{
nestedEntry[[aResult relativePath]] = @([rank integerValue]+1);
}
entries[[aResult name]] = nestedEntry;
}
[self saveDefaults:entries];
}
- (void)saveDefaults:(NSMutableDictionary *)entries
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:entries forKey:@"nestedResultSortRanks"];
}
- (void)purgeDictionary:(NSMutableDictionary *)entries
{
NSInteger threshold = 0;
while(entries.count > 250)
{
++threshold;
@autoreleasepool {
NSMutableArray *entriesToRemove = [NSMutableArray array];
__block NSMutableDictionary *toReplace = [NSMutableDictionary dictionary];
[entries enumerateKeysAndObjectsUsingBlock:^(id entryKey, id entry, BOOL *stop) {
NSMutableArray *nestedToRemove = [NSMutableArray array];
[entry enumerateKeysAndObjectsUsingBlock:^(id nestedKey, id nested, BOOL *stop2) {
if([nested integerValue] <= threshold)
{
[nestedToRemove addObject:nestedKey];
}
}];
entry = [NSMutableDictionary dictionaryWithDictionary:entry];
[entry removeObjectsForKeys:nestedToRemove];
if([entry count] == 0)
{
[entriesToRemove addObject:entryKey];
}
else
{
toReplace[entryKey] = entry;
}
}];
[entries removeObjectsForKeys:entriesToRemove];
[toReplace enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
entries[key] = obj;
}];
}
}
for(NSString *entryKey in [entries allKeys])
{
NSMutableDictionary *nestedEntry = entries[entryKey];
for(NSString *nestedKey in [nestedEntry allKeys])
{
nestedEntry[nestedKey] = @([nestedEntry[nestedKey] integerValue]-threshold);
}
}
}
@end
================================================
FILE: Dash/DHDBResult.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHDocset.h"
#import "DHDBResultSorter.h"
@interface DHDBResult : NSObject
@property (retain) NSString *path, *anchor, *relativePath, *fullPath, *name, *originalName, *type, *platform;
@property (retain) DHDocset *docset;
@property (retain) NSMutableArray *similarResults;
@property (retain) NSString *query;
@property (retain) NSString *_declaredInPage;
@property (retain) NSNumber *distanceFromQuery;
@property (assign) BOOL isHTTP, isSO, isPHP, isRust, isGo, isSwift;
@property (assign) BOOL perfectMatch, queryIsPrefix, queryIsSuffix, perfectMatchOriginal, queryIsPrefixOfOriginal, queryIsSuffixOfOriginal, matchesQueryAtAll, fuzzyCamel, fuzzyPerfect, fuzzy, whitespaceMatch, originalMatchesQueryAtAll, fuzzyShouldIgnore;
@property (retain) NSMutableArray *highlightRanges;
@property (assign) NSInteger fragmentation;
@property (assign) NSInteger actualFragmentation;
@property (assign) BOOL isApple;
@property (assign) NSInteger appleLanguage;
@property (assign) BOOL linkIsSwift;
@property (assign) NSInteger score;
@property (assign) BOOL isAGuide;
@property (assign) BOOL isActive;
@property (assign) BOOL isRemote;
@property (retain) UIImage *_typeImage;
@property (retain) UIImage *_platformImage;
@property (retain) NSString *remoteDocsetName;
@property (retain) NSString *remoteResultURL;
@property (retain) NSString *menuDescription; // used to build the declaredInPage, set it using #
@property (retain) NSString *appleObjCPath;
@property (retain) NSString *appleSwiftPath;
+ (DHDBResult *)resultWithDocset:(DHDocset *)docset resultSet:(FMResultSet *)rs;
- (void)prepareName;
- (UIImage *)typeImage;
- (UIImage *)platformImage;
- (NSString *)declaredInPage;
- (void)highlightLabel:(UILabel *)label;
- (void)highlightWithQuery:(NSString *)aQuery;
+ (NSDictionary *)highlightDictionary;
- (NSComparisonResult)compare:(DHDBResult *)aResult;
- (NSComparisonResult)levenshteinCompare:(DHDBResult *)aResult;
- (NSComparisonResult)compareFuziness:(DHDBResult *)aResult;
- (float)levenshteinDistance;
- (NSString *)sortType;
- (NSString *)duplicateHash;
- (NSString *)browserDuplicateHash;
- (DHDBResult *)activeResult;
- (NSUInteger)indexOfActiveItem;
- (void)setActiveItemByIndex:(NSUInteger)index;
- (NSString *)webViewURL;
@end
================================================
FILE: Dash/DHDBResult.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDBResult.h"
#import "DHAppDelegate.h"
#import "DHCSS.h"
@implementation DHDBResult
static NSSet *commonDeclaredInStylePlatforms;
static NSDictionary *highlightDictionary;
+ (DHDBResult *)resultWithDocset:(DHDocset *)docset resultSet:(FMResultSet *)rs
{
return [[DHDBResult alloc] initWithDocset:docset resultSet:rs];
}
- (id)initWithDocset:(DHDocset *)docset resultSet:(FMResultSet *)rs
{
self = [super init];
if(self)
{
self.docset = docset;
self.platform = self.docset.platform;
self.isSO = [self.platform isEqualToString:@"soonline"] || [self.platform isEqualToString:@"sooffline"];
self.path = [rs stringForColumnIndex:0];
if(!self.path.length)
{
return nil;
}
NSRange anchorRange = [self.path rangeOfString:@"#"];
if(anchorRange.location != NSNotFound && anchorRange.location+1 < self.path.length)
{
self.anchor = [self.path substringFromDashIndex:anchorRange.location+1];
self.path = [self.path substringToDashIndex:anchorRange.location];
}
if([self.platform isEqualToString:@"apple"])
{
NSInteger activeLanguage = [DHAppleActiveLanguage currentLanguage];
if([self.anchor contains:@""] || [self.anchor contains:@""])
{
if(activeLanguage == DHNewActiveAppleLanguageSwift)
{
return nil;
}
self.appleLanguage = DHNewActiveAppleLanguageObjC;
self.anchor = [self.anchor stringByReplacingOccurrencesOfString:@"" withString:@""];
self.anchor = [self.anchor stringByReplacingOccurrencesOfString:@"" withString:@""];
}
else if([self.anchor contains:@""])
{
if(activeLanguage == DHNewActiveAppleLanguageObjC)
{
return nil;
}
self.appleLanguage = DHNewActiveAppleLanguageSwift;
self.anchor = [self.anchor stringByReplacingOccurrencesOfString:@"" withString:@""];
}
}
BOOL isOSX = ([self.platform isEqualToString:@"macosx"] || [self.platform isEqualToString:@"osx"]);
if(self.anchor && isOSX && [self.anchor rangeOfString:@"java" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
return nil;
}
self.type = [rs stringForColumnIndex:2];
if(!self.type.length)
{
return nil;
}
self.isApple = (isOSX || [[self platform] isEqualToString:@"ios"] || [[self platform] isEqualToString:@"iphoneos"] || [self.platform isEqualToString:@"watchos"] || [self.platform isEqualToString:@"tvos"]);
if(self.isApple)
{
if([self.anchor hasSuffix:@"-dash-swift-hack"])
{
self.linkIsSwift = YES;
self.anchor = [self.anchor substringToIndex:self.anchor.length-16];
}
self.linkIsSwift = self.linkIsSwift || [self.anchor contains:@"apple_ref/swift/"];
int active = [DHCSS activeAppleLanguage];
if(active == DHActiveAppleLanguageObjC && ![docset.name contains:@"Xcode"])
{
if(self.linkIsSwift)
{
return nil;
}
}
else if(active == DHActiveAppleLanguageSwift)
{
BOOL isObjC = [self.anchor contains:@"apple_ref/occ"];
if(isObjC)
{
return nil;
}
}
}
BOOL isOnlineGuide = NO;
if([self.type isEqualToString:@"Guide"] || [self.type isEqualToString:@"Sample"])
{
if([self.path hasPrefix:@"gfile://"])
{
self.isAGuide = YES;
self.path = [self.path substringFromDashIndex:8];
self.fullPath = self.path;
self.relativePath = self.path;
}
else if([self.path hasPrefix:@"ghttp://"] || [self.path hasPrefix:@"ghttps://"])
{
self.isAGuide = YES;
isOnlineGuide = YES;
self.path = [self.path substringFromDashIndex:8];
self.fullPath = self.path;
self.relativePath = self.path;
}
}
if(!isOnlineGuide)
{
if([self.path hasPrefix:@"http://"] || [self.path hasPrefix:@"https://"] || self.isSO || [self.path hasPrefix:@"dash-apple-api://"])
{
self.isHTTP = YES;
self.fullPath = self.path;
if([self.platform isEqualToString:@"sooffline"])
{
self.fullPath = [self.fullPath stringByAppendingFormat:@"?dbPath=%@", [self.docset.sqlPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
}
}
else
{
self.fullPath = [@"file://" stringByAppendingString:[[self.docset.path stringByAppendingPathComponent:@"Contents/Resources/Documents"] stringByAppendingPathComponent:self.path]];
}
self.fullPath = (self.anchor) ? [self.fullPath stringByAppendingFormat:@"#%@", self.anchor] : self.fullPath;
self.relativePath = (self.anchor) ? [self.path stringByAppendingFormat:@"#%@", self.anchor] : self.path;
}
self.name = [rs stringForColumnIndex:1];
if(!self.name.length)
{
return nil;
}
self.originalName = self.name;
self.similarResults = [NSMutableArray array];
[self prepareName];
}
return self;
}
- (void)prepareName
{
if(self.isSO)
{
if([self.anchor hasPrefix:@"dash-score-"])
{
self.score = [[self.anchor substringFromIndex:11] integerValue];
}
}
if(self.isApple)
{
self.isApple = YES;
}
if([self.platform isEqualToString:@"unity3d"])
{
self.name = [self.name stringByReplacingOccurrencesOfString:@"%47" withString:@"/"];
self.originalName = [self.originalName stringByReplacingOccurrencesOfString:@"%47" withString:@"/"];
}
if([self.platform isEqualToString:@"go"] || [self.platform isEqualToString:@"godoc"])
{
self.isGo = YES;
}
NSString *shorteningFamily = self.docset.nameShorteningFamily;
NSString *parseFamily = (shorteningFamily) ? shorteningFamily : self.docset.parseFamily;
parseFamily = (parseFamily && parseFamily.length) ? parseFamily : self.platform;
if(([parseFamily isEqualToString:@"python"] || [parseFamily isEqualToString:@"flask"] || [parseFamily isEqualToString:@"scipy"] || [parseFamily isEqualToString:@"numpy"] || [parseFamily isEqualToString:@"pandas"] || [parseFamily isEqualToString:@"sqlalchemy"] || [parseFamily isEqualToString:@"tornado"] || [parseFamily isEqualToString:@"matplotlib"] || [parseFamily isEqualToString:@"salt"] || [parseFamily isEqualToString:@"jinja"] || ([self.platform isEqualToString:@"ocaml"] && ([self.type isEqualToString:@"Type"] || [self.type isEqualToString:@"Value"])) || [parseFamily isEqualToString:@"mono"] || [parseFamily isEqualToString:@"xamarin"] || [parseFamily isEqualToString:@"sencha"] || [parseFamily isEqualToString:@"extjs"] || [parseFamily isEqualToString:@"titanium"] || [parseFamily isEqualToString:@"twisted"] || [parseFamily isEqualToString:@"unity3d"] || [parseFamily isEqualToString:@"django"] || ([parseFamily isEqualToString:@"javascript"] && ![self.type isEqualToString:@"Function"] && ![self.type isEqualToString:@"Keyword"]) || [parseFamily isEqualToString:@"actionscript"] || [parseFamily isEqualToString:@"yui"] || [parseFamily isEqualToString:@"vsphere"] || ([self.platform isEqualToString:@"SproutCore"] && ![self.type isClassType] && ![self.type isEqualToString:@"Protocol"] && ![self.type isEqualToString:@"Delegate"])) && ![self.type isPackageType])
{
self.name = [self.name lastPackageComponent:@"."];
}
else if([parseFamily isEqualToString:@"apple"])
{
self.name = [self.name substringFromLastOccurrenceOfString:@"."];
}
else if([parseFamily isEqualToString:@"jQuery"] || [parseFamily isEqualToString:@"jqueryui"])
{
if([self.name hasPrefix:@"."])
{
self.name = [self.name substringFromDashIndex:1];
}
else if([self.name hasPrefix:@":"])
{
self.name = [self.name substringFromDashIndex:1];
}
else if([self.name hasCaseInsensitivePrefix:@"jQuery."])
{
self.name = [self.name substringFromDashIndex:@"jQuery.".length];
}
}
else if([self.platform isEqualToString:@"net"] && ![@[@"Class", @"Delegate", @"Interface", @"Namespace", @"Constructor", @"Enum", @"Struct", @"Conversion"] containsObject:self.type])
{
self.name = [self.name substringFromString:@"."];
}
else if([self.platform isEqualToString:@"matlab"])
{
if([self.type isEqualToString:@"Class"])
{
self.name = [self.name lastPackageComponent:@"."];
}
}
else if([self.platform isEqualToString:@"handlebars"])
{
if([self.type isEqualToString:@"Method"])
{
self.name = [self.name substringFromLastOccurrenceOfString:@"."];
}
}
else if([parseFamily isEqualToString:@"lodash"])
{
if([self.name hasPrefix:@"_."])
{
self.name = [self.name substringFromIndex:2];
}
}
else if([parseFamily isEqualToString:@"jquerym"])
{
if([self.name hasCaseInsensitivePrefix:@"jQuery.mobile."])
{
self.name = [self.name substringFromDashIndex:@"jQuery.mobile.".length];
}
}
else if([parseFamily isCaseInsensitiveEqual:@"smarty"])
{
self.name = [self.name substringFromString:@"->"];
}
else if(([parseFamily isEqualToString:@"scala"] || [parseFamily isEqualToString:@"scaladoc"] || [parseFamily isEqualToString:@"playscala"] || [parseFamily isEqualToString:@"akka"]) && ![self.type isPackageType])
{
self.name = [self.name lastPackageComponent:@"."];
}
else if([parseFamily isEqualToString:@"ember"] && ![self.type isPackageType] && ![self.type isClassType] && ![self.type isEqualToString:@"Guide"])
{
self.name = [self.name lastPackageComponent:@"."];
}
else if([self.platform isEqualToString:@"erlang"] || [parseFamily isEqualToString:@"erlang_shortening"])
{
self.name = [self.name lastPackageComponent:@":"];
NSInteger location = [self.name rangeOfString:@"/"].location;
if(location != NSNotFound && location != 0)
{
self.name = [self.name substringToDashIndex:location];
}
}
else if([self.platform isEqualToString:@"elixir"] || [self.platform isEqualToString:@"hex"] || [parseFamily isEqualToString:@"elixir_shortening"])
{
if(![@[@"Exception", @"Protocol", @"Module"] containsObject:self.type] && ![self.type isPackageType] && ![self.type isClassType] && ![self.type isEqualToString:@"Guide"])
{
self.name = [self.name lastPackageComponent:@"."];
self.name = [self.name substringToLastOccurrenceOfString:@"/"];
}
}
else if(([self.platform isEqualToString:@"dartlang"] || [parseFamily isEqualToString:@"dartlang"] || [self.platform isEqualToString:@"polymerdart"] || [self.platform isEqualToString:@"angulardart"]) && ![self.type isPackageType])
{
if([self.type isEqualToString:@"Constructor"])
{
if([[self.name substringFromLastOccurrenceOfString:@"."] firstCharIsLowercase])
{
self.name = [self.name lastTwoPackageComponents:@"."];
}
else
{
self.name = [self.name substringFromLastOccurrenceOfString:@"."];
}
}
else
{
self.name = [self.name substringFromLastOccurrenceOfString:@"."];
}
}
else if((([parseFamily isEqualToString:@"go"] || [self.platform isEqualToString:@"godoc"]) && ![self.type isEqualToString:@"Package"]) || ([self.platform isEqualToString:@"zepto"] && ![self.type isEqualToString:@"Module"]))
{
self.name = [self.name substringFromString:@"."];
}
else if(([parseFamily isEqualToString:@"compass"] && ![self.type isEqualToString:@"Module"]) || [parseFamily isEqualToString:@"dojo"])
{
self.name = [self.name lastPackageComponent:@"/"];
}
else if([self.platform isEqualToString:@"ee"])
{
self.name = [self.name substringFromString:@"::"];
}
else if([parseFamily isEqualToString:@"cappuccino"] || [self.platform isEqualToString:@"cvcpp"] || [self.platform isEqualToString:@"drupal"] || [self.platform isEqualToString:@"zend"] || [self.platform isEqualToString:@"cocos2dx"] || [self.platform isEqualToString:@"doxy"] || [self.platform isEqualToString:@"doxygen"] || [parseFamily isEqualToString:@"doxy"] || [parseFamily isEqualToString:@"doxygen"])
{
self.name = [self.name lastPackageComponent:@"::"];
}
else if([self.platform isEqualToString:@"php"])
{
self.name = [self.name lastPackageComponent:@"::"];
self.isPHP = YES;
}
else if([self.platform isEqualToString:@"rust"])
{
if(![self.type isEqualToString:@"Module"])
{
self.name = [self.name lastPackageComponent:@"::"];
}
self.isRust = YES;
}
else if([self.platform isEqualToString:@"swift"])
{
if([@[@"Method", @"Variable", @"Constant", @"Alias"] containsObject:self.type])
{
self.name = [self.name substringFromString:@"."];
}
self.isSwift = YES;
}
else if([self.platform isEqualToString:@"wordpress"] && [self.type isEqualToString:@"Method"]&& [self.name contains:@"::"])
{
self.name = [self.name substringFromLastOccurrenceOfString:@"::"];
}
else if([parseFamily isEqualToString:@"prototype"] && [self.type isEqualToString:@"Constructor"] && [self.name hasCaseInsensitivePrefix:@"new "])
{
self.name = [self.name substringFromDashIndex:4];
}
else if([parseFamily isEqualToString:@"cpp"] || [parseFamily isEqualToString:@"cocos2dx"])
{
self.name = [[self.name lastPackageComponent:@"::"] stringByUnescapingFromHTML];
self.originalName = [self.originalName stringByUnescapingFromHTML];
}
else if([self.platform isEqualToString:@"awsjs"])
{
if(![@[@"Guide", @"Section", @"Sample"] containsObject:self.type])
{
if(![self.type isPackageType] && ![self.type isClassType])
{
self.name = [self.name substringFromString:@"."];
}
}
}
else if([parseFamily isEqualToString:@"ruby"] || [parseFamily isEqualToString:@"rubyGems"] || [parseFamily isEqualToString:@"rails"])
{
if([parseFamily isEqualToString:@"rails"])
{
self.name = [self.name stringByUnescapingFromHTML];
self.originalName = [self.originalName stringByUnescapingFromHTML];
}
if(![@[@"Guide", @"Section", @"Sample"] containsObject:self.type])
{
if([self.name contains:@"::"])
{
self.name = [self.name lastPackageComponent:@"::"];
}
if(![self.type isPackageType] && ![self.type isClassType])
{
if([self.name contains:@"#"])
{
self.name = [self.name substringFromString:@"#"];
}
else
{
self.name = [self.name substringFromString:@"."];
}
}
}
}
else if([self.platform isEqualToString:@"laravel"] || [self.platform isEqualToString:@"phpp"] || [parseFamily isEqualToString:@"phpShortening"] || [self.platform isEqualToString:@"joomla"] || [self.platform isEqualToString:@"symfony"] || [self.platform isEqualToString:@"cakephp"] || [self.platform isEqualToString:@"typo3"])
{
if(![self.type isPackageType] && ![self.type isEqualToString:@"Function"])
{
self.name = [self.name lastPackageComponent:@"\\"];
self.name = [self.name lastPackageComponent:@"::"];
}
}
else if(([parseFamily isEqualToString:@"yard"] || [parseFamily isEqualToString:@"qt"] || [parseFamily isEqualToString:@"yii"]) && [self.name rangeOfString:@"::"].location != NSNotFound)
{
self.name = [self.name lastPackageComponent:@"::"];
}
else if([parseFamily isEqualToString:@"manPages"])
{
NSInteger loc = [self.name rangeOfString:@"(" options:NSBackwardsSearch].location;
if(loc != NSNotFound)
{
self.name = [self.name substringToDashIndex:loc];
}
}
else if([parseFamily isEqualToString:@"java"] || [parseFamily isEqualToString:@"playjava"] || [parseFamily isEqualToString:@"javafx"] || [parseFamily isEqualToString:@"groovy"])
{
self.name = [self.name stringByUnescapingFromHTML];
self.originalName = self.name;
}
if([self.fullPath contains:@""] stringByReplacingPercentEscapes];
if(newName.length)
{
self.name = newName;
}
}
if([self.fullPath contains:@""] stringByReplacingPercentEscapes];
if(newOriginalName.length)
{
self.originalName = newOriginalName;
}
}
if([self.fullPath contains:@""] stringByReplacingPercentEscapes];
if(newMenuDescription.length)
{
self.menuDescription = newMenuDescription;
}
}
if([self.fullPath contains:@""] stringByReplacingPercentEscapes];
if(objcPath.length)
{
self.appleObjCPath = objcPath;
}
}
if([self.fullPath contains:@""] stringByReplacingPercentEscapes];
if(swift.length)
{
self.appleSwiftPath = swift;
}
}
while([self.fullPath contains:@""];
if(toRemove.length)
{
toRemove = [NSString stringWithFormat:@"", toRemove];
self.fullPath = [self.fullPath stringByReplacingOccurrencesOfString:toRemove withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, self.fullPath.length)];
self.relativePath = [self.relativePath stringByReplacingOccurrencesOfString:toRemove withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, self.relativePath.length)];
self.path = [self.path stringByReplacingOccurrencesOfString:toRemove withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, self.path.length)];
}
else
{
break;
}
}
}
- (NSString *)description
{
return [NSString stringWithFormat:@"Name: %@, Full Path: %@, Platform: %@, Type: %@, Similars: %@", self.name, self.fullPath, self.platform, self.type, self.similarResults];
}
- (BOOL)isEqual:(DHDBResult *)someResult
{
NSString *myFamily = self.docset.parseFamily;
NSString *theirFamily = someResult.docset.parseFamily;
if(self.isSO && someResult.isSO)
{
return [self.path isEqualToString:someResult.path];
}
return ([self.name isEqualToString:someResult.name] && [self.type isEqualToString:someResult.type] && ([self.platform isEqualToString:someResult.platform] || (myFamily.length && [myFamily isEqualToString:@"cheatsheet"] && theirFamily.length && [myFamily isEqualToString:theirFamily])));
}
- (UIImage *)typeImage
{
if(self._typeImage)
{
return self._typeImage;
}
UIImage *image = [UIImage imageNamed:self.type];
return image;
}
- (UIImage *)platformImage
{
if(self._platformImage)
{
return self._platformImage;
}
return self.docset.icon;
}
- (NSString *)declaredInPage
{
if(self._declaredInPage)
{
return self._declaredInPage;
}
if(self.menuDescription)
{
self._declaredInPage = [@" - " stringByAppendingString:self.menuDescription];
return self._declaredInPage;
}
if(self.isAGuide)
{
NSString *toAppend = self.originalName;
for(NSString *token in @[@"/Conceptual/", @"/GettingStarted/", @"/General/", @"releasenotes/", @"samplecode/", @"featuredarticles/", @"technotes/", @"documentation/", @"/"])
{
NSRange tokenRange = [self.path rangeOfString:token];
if(tokenRange.location != NSNotFound)
{
NSString *guideName = [[self.path substringFromDashIndex:NSMaxRange(tokenRange)] substringToString:@"/"];
guideName = [guideName removePrefixIfExists:@"DevPedia-"];
guideName = [guideName removePrefixIfExists:@"xcode_guide-"];
guideName = [guideName stringByReplacingOccurrencesOfString:@"_" withString:@" "];
toAppend = [guideName stringByAppendingFormat:@" - %@", self.originalName];
break;
}
}
self._declaredInPage = [@" - " stringByAppendingString:toAppend];
return self._declaredInPage;
}
if([self.platform isEqualToString:@"lisp"])
{
self._declaredInPage = [@" - " stringByAppendingString:[self.path lastPathComponent]];
return self._declaredInPage;
}
if([self.platform isEqualToString:@"ee"] && [self.originalName contains:@"::"])
{
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
if([self.platform isEqualToString:@"net"])
{
NSString *fqn = [self.relativePath substringFromStringReturningNil:@"#dashFQN"];
if(fqn && fqn.length)
{
fqn = [fqn stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
self._declaredInPage = [@" - " stringByAppendingString:fqn];
return self._declaredInPage;
}
else
{
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
}
if([self.platform isEqualToString:@"moo"])
{
if([[[[self.path lastPathComponent] stringByDeletingPathExtension] stringByDeletingPathFragment] contains:@"-"])
{
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
}
NSString *declaredInStyle = self.docset.declaredInStyle;
NSString *parseFamily = self.docset.parseFamily;
parseFamily = (declaredInStyle) ? declaredInStyle : parseFamily;
if([[DHDBResult commonDeclaredInStylePlatforms] containsObject:self.platform] || ([self.platform isEqualToString:@"wordpress"] && [self.originalName contains:@"::"] && [self.type isEqualToString:@"Method"]) || ([self.platform isEqualToString:@"matlab"] && [self.type isEqualToString:@"Class"]) || ([self.platform isEqualToString:@"actionscript"] && ![self.type isEqualToString:@"Class"]) || ([self.platform isEqualToString:@"grails"] && [self.type isEqualToString:@"Guide"]) || [parseFamily isEqualToString:@"cheatsheet"] || [parseFamily isEqualToString:@"originalName"])
{
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
else if([self.platform isEqualToString:@"cappuccino"] || [self.platform isEqualToString:@"zend"] || [self.platform isEqualToString:@"typo3"] || [self.platform isEqualToString:@"cocos2dx"] || [self.platform isEqualToString:@"doxy"] || [self.platform isEqualToString:@"doxygen"] || [parseFamily isEqualToString:@"doxy"] || [parseFamily isEqualToString:@"doxygen"])
{
if([self.type isClassType] || [self.type isPackageType] || [self.type isEqualToString:@"Interface"] || [self.type isEqualToString:@"Exception"] || [self.type isEqualToString:@"Union"] || [self.type isEqualToString:@"Struct"] || [self.type isEqualToString:@"Guide"] || [self.type isEqualToString:@"Sample"] || [self.type isEqualToString:@"Protocol"] || [self.type isEqualToString:@"Delegate"])
{
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
NSString *filename = [[[self.path lastPathComponent] stringByDeletingPathExtension] stringByDeletingPathFragment];
if([filename hasCaseInsensitivePrefix:@"dir_"])
{
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
if(filename.length == 128)
{
filename = [filename substringToDashIndex:96];
NSRange alphaRange = [filename rangeOfCharacterFromSet:[NSCharacterSet lowercaseLetterCharacterSet] options:NSBackwardsSearch];
if(alphaRange.location != NSNotFound)
{
filename = [filename substringToDashIndex:alphaRange.location+1];
filename = [filename stringByAppendingString:@"..."];
}
else
{
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
}
if([filename hasCaseInsensitivePrefix:@"group_"])
{
filename = [filename substringFromDashIndex:6];
}
if([filename hasCaseInsensitivePrefix:@"category"])
{
filename = [filename substringFromDashIndex:8];
}
else if([filename hasCaseInsensitivePrefix:@"interface"])
{
filename = [filename substringFromDashIndex:9];
}
else if([filename hasCaseInsensitivePrefix:@"class"])
{
filename = [filename substringFromDashIndex:5];
}
else if([filename hasCaseInsensitivePrefix:@"namespace"])
{
filename = [filename substringFromDashIndex:9];
}
else if([filename hasCaseInsensitivePrefix:@"struct"])
{
filename = [filename substringFromDashIndex:6];
}
else if([filename hasCaseInsensitivePrefix:@"union"])
{
filename = [filename substringFromDashIndex:5];
}
NSString *declaredName = [[[[[[[[[[[[[[[[[[[[[[[[filename stringByReplacingOccurrencesOfString:@"_1" withString:@":"] stringByReplacingOccurrencesOfString:@"_2" withString:@"/"] stringByReplacingOccurrencesOfString:@"_3" withString:@"<"] stringByReplacingOccurrencesOfString:@"_4" withString:@">"] stringByReplacingOccurrencesOfString:@"_5" withString:@"*"] stringByReplacingOccurrencesOfString:@"_6" withString:@"&"] stringByReplacingOccurrencesOfString:@"_7" withString:@"|"] stringByReplacingOccurrencesOfString:@"_9" withString:@"!"] stringByReplacingOccurrencesOfString:@"_00" withString:@","] stringByReplacingOccurrencesOfString:@"_01" withString:@" "] stringByReplacingOccurrencesOfString:@"_02" withString:@"{"] stringByReplacingOccurrencesOfString:@"_03" withString:@"}"] stringByReplacingOccurrencesOfString:@"_04" withString:@"?"] stringByReplacingOccurrencesOfString:@"_05" withString:@"^"] stringByReplacingOccurrencesOfString:@"_06" withString:@"%"] stringByReplacingOccurrencesOfString:@"_07" withString:@"("] stringByReplacingOccurrencesOfString:@"_08" withString:@")"] stringByReplacingOccurrencesOfString:@"_09" withString:@"+"] stringByReplacingOccurrencesOfString:@"_0A" withString:@"="] stringByReplacingOccurrencesOfString:@"_0B" withString:@"$"] stringByReplacingOccurrencesOfString:@"_0C" withString:@"\\"] stringByReplacingOccurrencesOfString:@"_8" withString:@"."] stringByReplacingOccurrencesOfString:@"__" withString:@" "] stringByReplacingOccurrencesOfString:@"::" withString:@"\\"];
NSRange underRange = [declaredName rangeOfString:@"_"];
while(underRange.location != NSNotFound)
{
if(underRange.location+2 <= declaredName.length)
{
declaredName = [declaredName stringByReplacingCharactersInRange:NSMakeRange(underRange.location, 2) withString:[[declaredName substringWithDashRange:NSMakeRange(underRange.location+1, 1)] uppercaseString]];
underRange = [declaredName rangeOfString:@"_"];
}
else
{
break;
}
}
if(declaredName.length)
{
self._declaredInPage = [NSString stringWithFormat:@" - %@ > %@", [declaredName stringByReplacingOccurrencesOfString:@" " withString:@"_"], self.originalName];
return self._declaredInPage;
}
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
else if([self.platform isEqualToString:@"manPages"])
{
NSInteger anchorLoc = [self.fullPath rangeOfString:@"#"].location;
if(anchorLoc != NSNotFound && anchorLoc+1 < self.fullPath.length)
{
self._declaredInPage = [@" - " stringByAppendingString:[self.fullPath substringFromDashIndex:anchorLoc+1]];
return self._declaredInPage;
}
self._declaredInPage = [@" - " stringByAppendingString:self.originalName];
return self._declaredInPage;
}
NSString *thePath = self.path;
if([self.platform isEqualToString:@"clojure"])
{
thePath = [thePath stringByReplacingOccurrencesOfString:@"-api.html" withString:@".html"];
}
else if([@[@"ios", @"osx", @"macosx", @"iphoneos", @"watchos", @"tvos"] containsObject:self.platform])
{
thePath = [thePath stringByReplacingOccurrencesOfString:@"Swift/Reference/Swift_" withString:@"Swift/Reference/"];
thePath = [thePath stringByReplacingOccurrencesOfString:@"Reference/Reference.html" withString:@"index.html"];
thePath = [thePath stringByReplacingOccurrencesOfString:@"Introduction/Introduction.html" withString:@"index.html"];
}
if([self.platform isEqualToString:@"android"])
{
if([thePath hasCaseInsensitivePrefix:@"docs/"])
{
thePath = [thePath substringFromDashIndex:@"docs/".length];
}
else if([thePath hasCaseInsensitivePrefix:@"google_play_services_docs/"])
{
thePath = [thePath substringFromDashIndex:@"google_play_services_docs/".length];
}
}
NSArray *reverseObjects = [[[thePath pathComponents] reverseObjectEnumerator] allObjects];
int i = 0;
for(__strong NSString *reverseObject in reverseObjects)
{
if([self.platform isEqualToString:@"wordpress"])
{
reverseObject = [reverseObject stringByReplacingOccurrencesOfString:@"---" withString:@"/"];
}
if(![reverseObject isCaseInsensitiveEqual:@"reference"] && ![reverseObject isCaseInsensitiveEqual:@"index.html"] && ![reverseObject isCaseInsensitiveEqual:@"description"] && ![reverseObject isCaseInsensitiveEqual:@"Introduction"] && ![reverseObject hasCaseInsensitiveSuffix:@"_h"] && ![reverseObject isCaseInsensitiveEqual:@"package-summary.html"] && ![reverseObject isCaseInsensitiveEqual:@"rdoc"])
{
NSRange anchorRange = [reverseObject rangeOfString:@"#"];
if(anchorRange.location != NSNotFound && anchorRange.location != 0)
{
reverseObject = [reverseObject substringToDashIndex:anchorRange.location];
}
reverseObject = [reverseObject stringByReplacingOccurrencesOfString:@".html" withString:@""];
if(![self.platform isEqualToString:@"wordpress"] && ![self.platform isEqualToString:@"nginx"])
{
reverseObject = [[reverseObject stringByReplacingOccurrencesOfString:@"_" withString:@" "] stringByReplacingOccurrencesOfString:@"-" withString:@"_"];
}
if(([self.platform isEqualToString:@"android"] || [self.platform isEqualToString:@"java"] || (parseFamily && [parseFamily isEqualToString:@"java"]) || [self.platform isEqualToString:@"playjava"] || [self.platform isEqualToString:@"groovy"] || [self.platform isEqualToString:@"corona"] || [self.platform isEqualToString:@"javafx"] || [self.platform isEqualToString:@"rubymotion"]) && (i == reverseObjects.count || [reverseObjects[i] isCaseInsensitiveEqual:@"Documents"]))
{
break;
}
if(self._declaredInPage.length > 0)
{
self._declaredInPage = [reverseObject stringByAppendingFormat:@"%@%@", ([self.platform isEqualToString:@"rubymotion"]) ? @"::" : @".", self._declaredInPage];
}
else
{
self._declaredInPage = reverseObject;
}
if(![self.platform isEqualToString:@"corona"] && ![self.platform isEqualToString:@"android"] && !(parseFamily && [parseFamily isEqualToString:@"java"]) && ![self.platform isEqualToString:@"java"] && ![self.platform isEqualToString:@"playjava"] && ![self.platform isEqualToString:@"groovy"] && ![self.platform isEqualToString:@"javafx"] && ![self.platform isEqualToString:@"actionscript"] && ![self.platform isEqualToString:@"rubymotion"])
{
break;
}
}
++i;
}
if(self._declaredInPage.length > 0)
{
if([self.platform isEqualToString:@"corona"])
{
self._declaredInPage = [self._declaredInPage substringFromString:@"."];
self._declaredInPage = [self._declaredInPage substringFromString:@"."];
}
else if([self.platform isEqualToString:@"haskell"] || [self.platform isEqualToString:@"hackage"])
{
self._declaredInPage = [self._declaredInPage stringByReplacingOccurrencesOfString:@"_" withString:@"."];
}
else if([self.platform isEqualToString:@"elisp"] || [self.platform isEqualToString:@"d3"])
{
self._declaredInPage = [self._declaredInPage stringByReplacingOccurrencesOfString:@"_" withString:@" "];
}
else if([self.platform isEqualToString:@"glib"])
{
if([self._declaredInPage hasCaseInsensitivePrefix:@"glib_"])
{
self._declaredInPage = [self._declaredInPage substringFromDashIndex:@"glib_".length];
}
self._declaredInPage = [self._declaredInPage stringByReplacingOccurrencesOfString:@"_" withString:@" "];
}
self._declaredInPage = [@" - " stringByAppendingString:self._declaredInPage];
return self._declaredInPage;
}
self._declaredInPage = @"";
return @"";
}
- (NSString *)duplicateHash
{
if(self.isApple)
{
return [NSString stringWithFormat:@"%@%@%@", [[[self.originalName stringByReplacingOccurrencesOfString:@"(_" withString:@""] stringByReplacingOccurrencesOfString:@"(" withString:@""] stringByReplacingOccurrencesOfString:@")" withString:@""], self.type, [self.fullPath substringToString:@"#"]];
}
return nil;
}
- (NSString *)browserDuplicateHash
{
NSString *pathHack = self.fullPath;
if(self.isApple)
{
pathHack = self.path;
}
return [NSString stringWithFormat:@"%@%@%@", self.originalName, self.type, pathHack];
}
- (void)highlightLabel:(UILabel *)label
{
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:label.attributedText];
if(self.highlightRanges.count)
{
for(NSValue *aValue in self.highlightRanges)
{
NSRange range = [aValue rangeValue];
[string addAttributes:[DHDBResult highlightDictionary] range:range];
}
}
else
{
// This is needed when the name is not contained within originalName, like the C++ docset
// It's a basic workaround that only covers the easiest case
NSRange queryRange = [self.name rangeOfString:self.query options:NSCaseInsensitiveSearch];
if(queryRange.location != NSNotFound)
{
[string addAttributes:[DHDBResult highlightDictionary] range:queryRange];
}
}
label.attributedText = string;
}
- (void)highlightWithQuery:(NSString *)aQuery
{
self.query = aQuery;
self.highlightRanges = [NSMutableArray array];
if(self.query && [self.query length] > 0)
{
if((self.matchesQueryAtAll || self.originalMatchesQueryAtAll) && !self.whitespaceMatch)
{
self.fragmentation = -1;
NSString *toHighlight = self.query;
NSRange range;
NSInteger offset = 0;
NSString *substring = [NSString stringWithString:self.originalName];
NSRange nameRange = [self.originalName rangeOfString:self.name options:NSCaseInsensitiveSearch|NSBackwardsSearch];
while((range = [substring rangeOfString:toHighlight options:NSCaseInsensitiveSearch]).location != NSNotFound)
{
NSRange originalRange = NSMakeRange(range.location+offset, range.length);
NSRange intersectionRange = NSIntersectionRange(originalRange, nameRange);
if(intersectionRange.length > 0)
{
intersectionRange.location -= nameRange.location;
[self.highlightRanges addObject:[NSValue valueWithRange:intersectionRange]];
}
substring = [substring substringFromDashIndex:range.location+range.length];
offset += range.location+range.length;
}
}
else
{
// check if whitespace match
if([self.originalName rangeOfString:@" "].location != NSNotFound)
{
NSMutableCharacterSet *checkSet = [NSMutableCharacterSet characterSetWithCharactersInString:@" "];
for(int i = 0; i < 3; i++)
{
// for is needed so that matches like "ftp&" work with results like "FTP & SFTP" (Vagrant docset)
if(i == 1)
{
[checkSet formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
}
else if(i == 2)
{
[checkSet formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
}
NSMutableArray *removedRanges = [NSMutableArray array];
NSString *strippedOriginalName = [self.originalName stringByDeletingCharactersInSet:checkSet removedRanges:removedRanges];
NSString *strippedQuery = [self.query stringByDeletingCharactersInSet:checkSet];
NSRange queryRange = [strippedOriginalName rangeOfString:strippedQuery options:NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch];
if(queryRange.location != NSNotFound)
{
for(NSValue *removedRangeValue in removedRanges)
{
NSRange removedRange = [removedRangeValue rangeValue];
if(removedRange.location <= queryRange.location)
{
queryRange.location += removedRange.length;
}
else if(NSIntersectionRange(removedRange, queryRange).length == removedRange.length)
{
queryRange.length += removedRange.length;
}
}
if([[self.originalName substringWithDashRange:queryRange] contains:@" "])
{
[self setHighlightRanges:[NSMutableArray arrayWithObject:[NSValue valueWithRange:queryRange]] relativeToNameRange:[self.originalName rangeOfString:self.name options:NSCaseInsensitiveSearch|NSBackwardsSearch]];
self.fragmentation = -1;
return;
}
}
}
}
// find all occurences of letters in query
NSMutableDictionary *letters = [NSMutableDictionary dictionary];
[self.query enumerateLettersUsingBlock:^(NSString *letter) {
if(!letters[letter])
{
letters[letter] = [self.originalName rangesOfString:letter];
}
}];
// initial seed: add occurences of first letter
NSMutableArray *matches = [NSMutableArray array];
NSString *firstLetter = [self.query substringWithRange:NSMakeRange(0, 1)];
for(NSValue *rangeValue in letters[firstLetter])
{
[matches addObject:[NSMutableArray arrayWithObject:rangeValue]];
}
// spread initial seeds to cover all possible occurences of all letters
[[self.query substringFromIndex:1] enumerateLettersUsingBlock:^(NSString *letter) {
NSArray *letterRangeValues = letters[letter];
for(NSMutableArray *match in [NSArray arrayWithArray:matches])
{
for(NSValue *letterRangeValue in letterRangeValues)
{
NSRange letterRange = [letterRangeValue rangeValue];
if(NSMaxRange([[match lastObject] rangeValue]) < NSMaxRange(letterRange))
{
NSMutableArray *matchSpread = [NSMutableArray arrayWithArray:match];
[matchSpread addObject:letterRangeValue];
[matches addObject:matchSpread];
if(matches.count >= 80)
{
break;
}
}
}
[matches removeObjectIdenticalTo:match];
}
}];
// remove matches that do not have all letters matched
for(NSMutableArray *match in [NSArray arrayWithArray:matches])
{
if(match.count < self.query.length)
{
[matches removeObjectIdenticalTo:match];
}
}
// merge ranges
for(NSMutableArray *match in matches)
{
NSRange lastRange = NSMakeRange(NSNotFound, 0);
for(NSUInteger index = 0; index < match.count; index++)
{
NSRange range = [match[index] rangeValue];
if(NSMaxRange(lastRange) == range.location)
{
range = NSMakeRange(lastRange.location, lastRange.length+range.length);
match[index-1] = [NSValue valueWithRange:range];
[match removeObjectAtIndex:index];
--index;
}
lastRange = range;
}
}
// remove matches for which the first matched letter is not one of the following: first letter of self.name, preceded by a char in allowedChars, preceded by lowercase and is uppercase, is uppercase and the first match group contains a lowercase letter
NSRange nameRange = [self.originalName rangeOfString:self.name options:NSCaseInsensitiveSearch|NSBackwardsSearch];
NSCharacterSet *allowedChars = [NSCharacterSet characterSetWithCharactersInString:@".%_#-\\/:$@!*&+()~^=,<>'\"{}[]| "];
for(NSMutableArray *match in [NSArray arrayWithArray:matches])
{
BOOL found = NO;
for(NSValue *rangeValue in match)
{
NSRange range = [rangeValue rangeValue];
if(range.location <= nameRange.location && NSMaxRange(range) >= nameRange.location+1)
{
found = YES;
break;
}
}
if(!found)
{
BOOL shouldContinue = NO;
for(NSValue *rangeValue in match)
{
NSRange firstRange = [rangeValue rangeValue];
if(NSIntersectionRange(nameRange, firstRange).length > 0)
{
if(firstRange.location >= nameRange.location && firstRange.location+firstRange.length <= nameRange.location+nameRange.length && firstRange.location > 0)
{
NSString *previousChar = [self.originalName substringWithRange:NSMakeRange(firstRange.location-1, 1)];
if([previousChar rangeOfCharacterFromSet:allowedChars].location != NSNotFound)
{
shouldContinue = YES;
break;
}
NSString *matchedChar = [self.originalName substringWithRange:NSMakeRange(firstRange.location, 1)];
BOOL matchedIsUpper = [matchedChar isUppercase];
BOOL previousIsUpper = [previousChar isUppercase];
if(matchedIsUpper && !previousIsUpper)
{
shouldContinue = YES;
break;
}
if(matchedIsUpper && [[self.originalName substringWithRange:firstRange] rangeOfCharacterFromSet:[NSCharacterSet lowercaseLetterCharacterSet]].location != NSNotFound)
{
shouldContinue = YES;
break;
}
}
break;
}
}
if(shouldContinue)
{
continue;
}
[matches removeObjectIdenticalTo:match];
}
}
// check if camel case
for(NSMutableArray *match in matches)
{
NSRange firstRange = [match[0] rangeValue];
NSRange lastRange = [[match lastObject] rangeValue];
if(firstRange.location < nameRange.location || NSMaxRange(lastRange) > NSMaxRange(nameRange))
{
continue;
}
BOOL isCamel = YES;
for(NSValue *rangeValue in match)
{
NSRange range = [rangeValue rangeValue];
if(range.location == nameRange.location)
{
continue;
}
else
{
NSString *previousLetter = [self.originalName substringWithRange:NSMakeRange(range.location-1, 1)];
if([previousLetter isEqualToString:@" "])
{
isCamel = NO;
break;
}
NSString *myLetter = [self.originalName substringWithRange:NSMakeRange(range.location, 1)];
BOOL myLetterUpper = [[myLetter uppercaseString] isEqualToString:myLetter];
BOOL myLetterLower = [[myLetter lowercaseString] isEqualToString:myLetter];
BOOL previousLetterUpper = [[previousLetter uppercaseString] isEqualToString:previousLetter];
BOOL previousLetterLower = [[previousLetter lowercaseString] isEqualToString:previousLetter];
if(!((myLetterUpper && !myLetterLower && previousLetterLower) || (!myLetterUpper && myLetterLower && previousLetterLower && previousLetterUpper)))
{
isCamel = NO;
break;
}
}
}
if(isCamel)
{
self.fuzzyCamel = YES;
[self setHighlightRanges:match relativeToNameRange:nameRange];
return;
}
}
// find largest length (relative to self.name) matches
NSInteger largestLength = 0;;
NSMutableArray *largestLengthMatches = [NSMutableArray array];
for(NSMutableArray *match in matches)
{
NSInteger currentLength = 0;
for(NSValue *rangeValue in match)
{
currentLength += NSIntersectionRange(nameRange, [rangeValue rangeValue]).length;
}
if(currentLength > largestLength)
{
largestLength = currentLength;
[largestLengthMatches removeAllObjects];
}
if(currentLength == largestLength)
{
[largestLengthMatches addObject:match];
}
}
[matches removeAllObjects];
[matches addObjectsFromArray:largestLengthMatches];
// find match with lowest fragmentation
NSInteger lowestPerceivedFragmentation = NSIntegerMax;
NSInteger lowestFragmentation = NSIntegerMax;
NSMutableArray *bestMatch = nil;
for(NSMutableArray *match in matches)
{
NSInteger currentFragmentation = match.count;
NSInteger currentPerceivedFragmentation = 0;
NSRange previousRange = NSMakeRange(NSNotFound, 0);
for(NSValue *rangeValue in match)
{
NSRange range = [rangeValue rangeValue];
if(previousRange.location != NSNotFound)
{
NSString *between = [self.originalName substringWithRange:NSMakeRange(NSMaxRange(previousRange), range.location-NSMaxRange(previousRange))];
if([between rangeOfCharacterFromSet:[NSCharacterSet letterCharacterSet]].location == NSNotFound)
{
--currentFragmentation;
continue;
}
}
if(range.length == 1 && [self.query length] > 2)
{
++currentPerceivedFragmentation;
}
previousRange = range;
}
if(currentFragmentation == lowestFragmentation)
{
if(currentPerceivedFragmentation < lowestPerceivedFragmentation)
{
lowestPerceivedFragmentation = currentPerceivedFragmentation;
bestMatch = match;
}
}
else if(currentFragmentation < lowestFragmentation)
{
lowestFragmentation = currentFragmentation;
lowestPerceivedFragmentation = currentPerceivedFragmentation;
bestMatch = match;
}
}
BOOL isSnippet = NO; //[self isKindOfClass:[DHDBSnippetResult class]];
if(!isSnippet && (lowestPerceivedFragmentation > 1 || lowestFragmentation > 4 || !bestMatch))
{
self.fuzzyShouldIgnore = YES;
}
else if(bestMatch)
{
[self setHighlightRanges:bestMatch relativeToNameRange:nameRange];
self.fragmentation = lowestPerceivedFragmentation;
self.actualFragmentation = lowestFragmentation;
if(lowestPerceivedFragmentation <= 1 && lowestFragmentation <= 2)
{
self.fuzzyPerfect = YES;
}
else
{
self.fuzzy = YES;
}
}
}
}
}
- (void)setHighlightRanges:(NSMutableArray *)bestRanges relativeToNameRange:(NSRange)nameRange
{
for(NSValue *rangeValue in bestRanges)
{
NSRange range = [rangeValue rangeValue];
NSRange intersection = NSIntersectionRange(range, nameRange);
if(intersection.length > 0)
{
[self.highlightRanges addObject:[NSValue valueWithRange:NSMakeRange(intersection.location-nameRange.location, intersection.length)]];
}
}
}
- (NSComparisonResult)compare:(DHDBResult *)aResult
{
if(self.matchesQueryAtAll && aResult.matchesQueryAtAll)
{
if(self.whitespaceMatch && !aResult.whitespaceMatch)
{
return NSOrderedDescending;
}
else if(!self.whitespaceMatch && aResult.whitespaceMatch)
{
return NSOrderedAscending;
}
}
if(self.score > aResult.score)
{
return NSOrderedAscending;
}
else if(self.score < aResult.score)
{
return NSOrderedDescending;
}
if(self.queryIsPrefix && aResult.queryIsPrefix)
{
return [self levenshteinCompare:aResult];
}
if(self.queryIsSuffix && aResult.queryIsSuffix)
{
if(self.highlightRanges.count)
{
if(aResult.highlightRanges.count)
{
NSRange myFirstRange = [(self.highlightRanges)[0] rangeValue];
NSRange theirFirstRange = [(aResult.highlightRanges)[0] rangeValue];
if(myFirstRange.location < theirFirstRange.location)
{
return NSOrderedAscending;
}
else if(myFirstRange.location > theirFirstRange.location)
{
return NSOrderedDescending;
}
else if(self.name.length < aResult.name.length)
{
return NSOrderedAscending;
}
else if(self.name.length > aResult.name.length)
{
return NSOrderedDescending;
}
else
{
return [self.name localizedCaseInsensitiveCompare:[aResult name]];
}
}
else
{
return NSOrderedAscending;
}
}
return [self levenshteinCompare:aResult];
}
else
{
if(self.matchesQueryAtAll)
{
if(aResult.matchesQueryAtAll)
{
return [self.name localizedCaseInsensitiveCompare:[aResult name]];
}
else
{
return NSOrderedAscending;
}
}
else if(aResult.matchesQueryAtAll)
{
return NSOrderedDescending;
}
}
return [self.name localizedCaseInsensitiveCompare:[aResult name]];
}
- (NSComparisonResult)levenshteinCompare:(DHDBResult *)aResult
{
if(self.matchesQueryAtAll && aResult.matchesQueryAtAll)
{
if(self.whitespaceMatch && !aResult.whitespaceMatch)
{
return NSOrderedDescending;
}
else if(!self.whitespaceMatch && aResult.whitespaceMatch)
{
return NSOrderedAscending;
}
}
float myDistance = [self levenshteinDistance];
float theirDistance = [aResult levenshteinDistance];
if(myDistance < theirDistance)
{
return NSOrderedAscending;
}
else if(myDistance == theirDistance)
{
return [self.name localizedCaseInsensitiveCompare:[aResult name]];
}
else
{
return NSOrderedDescending;
}
}
- (float)levenshteinDistance
{
if(self.distanceFromQuery != nil)
{
return [self.distanceFromQuery floatValue];
}
float distance = [self.name distanceFromString:self.query];
self.distanceFromQuery = @(distance);
return distance;
}
- (NSComparisonResult)compareFuziness:(DHDBResult *)aResult
{
if(self.fragmentation == -1 || aResult.fragmentation == -1)
{
NSInteger resultSortOrder = [self compareResultSortOrder:aResult];
if(resultSortOrder != NSOrderedSame)
{
return resultSortOrder;
}
return [self compare:aResult];
}
if(self.highlightRanges.count > aResult.highlightRanges.count)
{
return NSOrderedDescending;
}
else if(self.highlightRanges.count < aResult.highlightRanges.count)
{
return NSOrderedAscending;
}
else
{
NSInteger resultSortOrder = [self compareResultSortOrder:aResult];
if(resultSortOrder != NSOrderedSame)
{
return resultSortOrder;
}
if(self.score > aResult.score)
{
return NSOrderedAscending;
}
else if(self.score < aResult.score)
{
return NSOrderedDescending;
}
return [self levenshteinCompare:aResult];
}
}
- (NSComparisonResult)compareResultSortOrder:(DHDBResult *)aResult
{
DHDBResultSorter *sorter = [DHDBResultSorter sharedSorter];
NSInteger myRank = [sorter rankForResult:self];
NSInteger otherRank = [sorter rankForResult:aResult];
if(myRank < otherRank)
{
return NSOrderedDescending;
}
else if(myRank > otherRank)
{
return NSOrderedAscending;
}
return NSOrderedSame;
}
- (NSString *)sortType
{
if(self.isPHP && [self.type isEqualToString:@"Function"])
{
return @"Class";
}
else if(self.isRust && [self.type isEqualToString:@"_Struct"])
{
return @"Class";
}
else if(self.isSwift && [self.type isEqualToString:@"Type"])
{
return @"Class";
}
else if(self.isGo && [self.type isEqualToString:@"Type"])
{
return @"Class";
}
else if(self.isApple && self.linkIsSwift && [self.type isEqualToString:@"Struct"] && [self.path contains:@"/Swift/Reference/"])
{
return @"Class";
}
return self.type;
}
+ (NSSet *)commonDeclaredInStylePlatforms
{
if(!commonDeclaredInStylePlatforms)
{
commonDeclaredInStylePlatforms = [NSSet setWithObjects:@"apache", @"python", @"zepto", @"cvp", @"cvc", @"mongodb", @"cvcpp", @"vagrant", @"cf", @"ansible", @"ocaml", @"twig", @"smarty", @"chef", @"php", @"express", @"bash", @"swift", @"extjs", @"titanium", @"sencha", @"markdown", @"latex", @"bourbon", @"cmake", @"awesome", @"jade", @"SproutCore", @"neat", @"moment", @"elasticsearch", @"xojo", @"lodash", @"statamic", @"drupal", @"phonegap", @"cordova", @"laravel", @"compass", @"haml", @"sass", @"bootstrap", @"ember", @"jasmine", @"perl", @"jquerym", @"jQuery", @"css", @"dartlang", @"phpunit", @"polymerdart", @"angulardart", @"xul", @"xslt", @"javascript", @"arduino", @"angularjs", @"emmet", @"chai", @"mongoose", @"react", @"grunt", @"sooffline", @"soonline", @"rust", @"flask", @"numpy", @"pandas", @"sqlalchemy", @"tornado", @"matplotlib", @"salt", @"jinja", @"require", @"scipy", @"go", @"godoc", @"prototype", @"puppet", @"stylus", @"sinon", @"gl2", @"gl3", @"gl4", @"jqueryui", @"underscore", @"backbone", @"marionette", @"coffee", @"yii", @"mono", @"xamarin", @"yui", @"tcl", @"erlang", @"vsphere", @"twisted", @"phpp", @"joomla", @"symfony", @"cakephp", @"scala", @"scaladoc", @"playscala", @"akka", @"sqlite", @"boost", @"unity3d", @"django", @"cpp", @"c", @"qt", @"rails", @"codeigniter", @"yard", @"ruby", @"awsjs", @"rubyGems", @"foundation", @"lua", @"dojo", @"elixir", @"knockout", @"meteor", nil];
}
return commonDeclaredInStylePlatforms;
}
+ (NSDictionary *)highlightDictionary
{
if(!highlightDictionary)
{
highlightDictionary = @{NSForegroundColorAttributeName: [[DHAppDelegate sharedDelegate].window.rootViewController.view.tintColor colorWithAlphaComponent:0.8]};
}
return highlightDictionary;
}
- (NSUInteger)indexOfActiveItem
{
if([self isActive])
{
return 0;
}
else
{
int index = 1;
for(DHDBResult *result in self.similarResults)
{
if([result isActive])
{
return index;
}
++index;
}
}
[self setIsActive:YES];
return 0;
}
- (void)setActiveItemByIndex:(NSUInteger)index
{
self.isActive = (index == 0) ? YES : NO;
int i = 1;
for(DHDBResult *result in self.similarResults)
{
result.isActive = (i == index) ? YES : NO;
++i;
}
}
- (DHDBResult *)activeResult
{
if(self.isActive)
{
return self;
}
for(DHDBResult *result in self.similarResults)
{
if(result.isActive)
{
return result;
}
}
return self;
}
- (NSString *)webViewURL
{
NSString *fullPath = nil;
if(self.isRemote)
{
if(self.remoteResultURL)
{
fullPath = self.remoteResultURL;
}
return fullPath;
}
fullPath = self.fullPath;
NSInteger currentLanguage = [DHAppleActiveLanguage currentLanguage];
if(fullPath.length && [self.platform isEqualToString:@"apple"] && currentLanguage != self.appleLanguage)
{
if(currentLanguage == DHNewActiveAppleLanguageObjC && self.appleObjCPath.length)
{
return [@"file://" stringByAppendingString:[[self.docset.path stringByAppendingPathComponent:@"Contents/Resources/Documents"] stringByAppendingPathComponent:self.appleObjCPath]];
}
else if(currentLanguage == DHNewActiveAppleLanguageSwift && self.appleSwiftPath.length)
{
return [@"file://" stringByAppendingString:[[self.docset.path stringByAppendingPathComponent:@"Contents/Resources/Documents"] stringByAppendingPathComponent:self.appleSwiftPath]];
}
NSString *delimiter = self.docset.plist[@"DashDocSetAppleObjCDelimiter"];
NSString *delimiterWithExtension = [delimiter stringByAppendingString:@".html"];
if(delimiter.length)
{
NSString *currentPath = [[fullPath substringFromStringReturningNil:@"://"] substringToString:@"#"];
if(currentLanguage == DHNewActiveAppleLanguageObjC && ![currentPath contains:delimiterWithExtension] && [[NSFileManager defaultManager] fileExistsAtPathOrInIndex:[[currentPath stringByDeletingPathExtension] stringByAppendingString:delimiterWithExtension]])
{
NSString *currentAnchor = [fullPath substringFromLastOccurrenceOfStringReturningNil:@"#"];
NSString *newPath = [fullPath substringToLastOccurrenceOfString:@"#"];
NSRange extensionRange = [newPath rangeOfString:@".html" options:NSCaseInsensitiveSearch|NSBackwardsSearch];
if(extensionRange.location != NSNotFound)
{
newPath = [newPath stringByReplacingCharactersInRange:extensionRange withString:delimiterWithExtension];
if(currentAnchor.length)
{
newPath = [newPath stringByAppendingFormat:@"#%@", currentAnchor];
}
return newPath;
}
}
if(currentLanguage == DHNewActiveAppleLanguageSwift && [currentPath contains:delimiterWithExtension] && [[NSFileManager defaultManager] fileExistsAtPathOrInIndex:[currentPath stringByReplacingOccurrencesOfString:delimiterWithExtension withString:@".html"]])
{
return [fullPath stringByReplacingOccurrencesOfString:delimiterWithExtension withString:@".html"];
}
}
}
return fullPath;
}
@end
================================================
FILE: Dash/DHDBResultSorter.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@class DHDBResult;
@interface DHDBResultSorter : NSObject
@property (retain) NSMutableDictionary *ranks;
@property (retain) NSString *lastIncreasedIdentifier;
@property (retain) NSTimer *rankIncreaseTimer;
@property (assign) CGPoint visiblePoint;
+ (DHDBResultSorter *)sharedSorter;
- (NSInteger)rankForResult:(DHDBResult *)aResult;
- (void)resultWasSelected:(DHDBResult *)aResult inTableView:(UITableView *)tableView;
- (void)saveDefaults:(NSMutableDictionary *)entries;
@end
================================================
FILE: Dash/DHDBResultSorter.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDBResultSorter.h"
#import "DHDBResult.h"
#import "DHAppDelegate.h"
@implementation DHDBResultSorter
+ (DHDBResultSorter *)sharedSorter
{
static dispatch_once_t pred;
static DHDBResultSorter *_resultSorter = nil;
dispatch_once(&pred, ^{
_resultSorter = [[DHDBResultSorter alloc] init];
[_resultSorter setUp];
});
return _resultSorter;
}
- (void)setUp
{
self.ranks = [[[NSUserDefaults standardUserDefaults] objectForKey:@"resultSortRanks"] mutableCopy];
if(!self.ranks)
{
self.ranks = [NSMutableDictionary dictionary];
}
}
- (void)resultWasSelected:(DHDBResult *)aResult inTableView:(UITableView *)tableView
{
if(aResult.isRemote)
{
return;
}
if(!isRegularHorizontalClass)
{
[self increaseRankNow:aResult];
return;
}
if([self.rankIncreaseTimer isValid])
{
[self.rankIncreaseTimer invalidate];
self.rankIncreaseTimer = nil;
}
self.visiblePoint = tableView.contentOffset;
self.rankIncreaseTimer = [NSTimer scheduledTimerWithTimeInterval:6.0f target:self selector:@selector(increaseRankForResult:) userInfo:@[aResult, tableView] repeats:NO];
}
- (void)increaseRankForResult:(NSTimer *)timer
{
DHDBResult *aResult = [timer userInfo][0];
if(aResult.isRemote)
{
return;
}
UITableView *tableView = [timer userInfo][1];
if(tableView.window && !tableView.isHidden && CGPointEqualToPoint(self.visiblePoint, tableView.contentOffset))
{
[self increaseRankNow:aResult];
}
}
- (void)increaseRankNow:(DHDBResult *)aResult
{
if(aResult.isRemote)
{
return;
}
NSString *identifier = [self identifierForResult:aResult];
if(!identifier || !identifier.length || (self.lastIncreasedIdentifier && [identifier isEqualToString:self.lastIncreasedIdentifier]))
{
return;
}
self.lastIncreasedIdentifier = identifier;
NSMutableDictionary *ranks = self.ranks;
if(ranks.count > 1000)
{
[self purgeDictionary:ranks];
}
NSNumber *rankNumber = ranks[identifier];
NSInteger rank = 0;
if(rankNumber)
{
rank = [rankNumber integerValue];
}
++rank;
ranks[identifier] = @(rank);
[self saveDefaults:ranks];
}
- (NSInteger)rankForResult:(DHDBResult *)aResult
{
NSNumber *rank = (self.ranks)[[self identifierForResult:aResult]];
if(rank)
{
return [rank integerValue];
}
return 0;
}
- (NSString *)identifierForResult:(DHDBResult *)aResult
{
return [[aResult name] stringByAppendingFormat:@" - %@", aResult.type];
}
- (void)saveDefaults:(NSMutableDictionary *)entries
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:entries forKey:@"resultSortRanks"];
}
- (void)purgeDictionary:(NSMutableDictionary *)ranks
{
NSInteger threshold = 0;
while(ranks.count > 500)
{
++threshold;
@autoreleasepool {
NSMutableArray *ranksToRemove = [NSMutableArray array];
[ranks enumerateKeysAndObjectsUsingBlock:^(id rankKey, id rank, BOOL *stop) {
if([rank integerValue] <= threshold)
{
[ranksToRemove addObject:rankKey];
}
}];
[ranks removeObjectsForKeys:ranksToRemove];
}
}
for(NSString *rank in [ranks allKeys])
{
ranks[rank] = @([ranks[rank] integerValue]-threshold);
}
}
@end
================================================
FILE: Dash/DHDBSearchController.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHDBSearcher.h"
@interface DHDBSearchController : NSObject
@property (assign) BOOL loading;
@property (retain) NSArray *docsets;
@property (retain) NSString *typeLimit;
@property (retain) NSMutableArray *results;
@property (retain) NSMutableArray *nextResults;
@property (weak) UISearchDisplayController *displayController;
@property (weak) UIViewController *viewController;
@property (retain) DHDBSearcher *searcher;
@property (retain) NSString *viewControllerTitle;
@property (assign) BOOL isRestoring;
+ (DHDBSearchController *)searchControllerWithDocsets:(NSArray *)docsets typeLimit:(NSString *)typeLimit viewController:(UIViewController *)viewController;
- (void)viewWillAppear;
- (void)viewDidAppear;
- (void)viewDidDisappear;
- (void)viewWillDisappear;
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- (void)hookToSearchDisplayController:(UISearchDisplayController *)displayController;
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender;
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder;
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder;
@end
================================================
FILE: Dash/DHDBSearchController.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDBSearchController.h"
#import "DHBrowserTableViewCell.h"
#import "DHDBResult.h"
#import "DHDocsetManager.h"
#import "DHDocsetBrowser.h"
#import "DHNestedViewController.h"
@implementation DHDBSearchController
+ (DHDBSearchController *)searchControllerWithDocsets:(NSArray *)docsets typeLimit:(NSString *)typeLimit viewController:(UIViewController *)viewController;
{
DHDBSearchController *controller = [[DHDBSearchController alloc] init];
controller.docsets = docsets;
controller.typeLimit = typeLimit;
controller.viewController = viewController;
controller.displayController = viewController.searchDisplayController;
controller.displayController.searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone;
[controller hookToSearchDisplayController:viewController.searchDisplayController];
[[NSNotificationCenter defaultCenter] addObserver:controller selector:@selector(traitCollectionDidChange:) name:DHWindowChangedTraitCollection object:nil];
return controller;
}
- (void)viewWillAppear
{
if(self.displayController.active)
{
}
}
- (void)viewDidAppear
{
if(self.displayController.active)
{
}
}
- (void)viewWillDisappear
{
if(self.displayController.active)
{
}
}
- (void)viewDidDisappear
{
if(self.displayController.active)
{
}
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(self.results.count && !self.loading)
{
[self.displayController.searchResultsTableView reloadData];
}
}
- (void)hookToSearchDisplayController:(UISearchDisplayController *)displayController
{
displayController.delegate = self;
displayController.searchResultsDataSource = self;
displayController.searchResultsDelegate = self;
}
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView
{
self.loading = YES;
tableView.allowsSelection = NO;
if(isIOS11)
{
if(@available(iOS 11.0, *))
{
tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
[tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
[tableView registerNib:[UINib nibWithNibName:@"DHLoadingCell" bundle:nil] forCellReuseIdentifier:@"DHLoadingCell"];
}
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
if([self.viewController respondsToSelector:@selector(searchDisplayControllerWillBeginSearch:)])
{
[(id)self.viewController searchDisplayControllerWillBeginSearch:controller];
}
self.loading = YES;
self.displayController.searchResultsTableView.allowsSelection = NO;
[self.displayController.searchResultsTableView reloadData];
}
- (void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller
{
if([self.viewController respondsToSelector:@selector(searchDisplayControllerDidBeginSearch:)])
{
[(id)self.viewController searchDisplayControllerDidBeginSearch:controller];
}
self.viewControllerTitle = self.viewController.navigationItem.title;
self.viewController.navigationItem.title = @"Search";
}
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
if([self.viewController respondsToSelector:@selector(searchDisplayControllerWillEndSearch:)])
{
[(id)self.viewController searchDisplayControllerWillEndSearch:controller];
}
self.viewController.navigationItem.title = self.viewControllerTitle;
[self.searcher cancelSearch];
self.searcher = nil;
}
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller
{
if([self.viewController respondsToSelector:@selector(searchDisplayControllerDidEndSearch:)])
{
[(id)self.viewController searchDisplayControllerDidEndSearch:controller];
}
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
if(self.isRestoring)
{
self.displayController.searchResultsTableView.allowsSelection = YES;
self.loading = NO;
return YES;
}
[self.searcher cancelSearch];
self.nextResults = [NSMutableArray array];
BOOL wasEmpty = searchString.length <= 0;
searchString = [searchString stringByRemovingWhitespaces];
if(searchString.length)
{
self.searcher = [DHDBSearcher searcherWithDocsets:(self.docsets) ? self.docsets : [(id)self.viewController shownDocsets] query:searchString limitToType:self.typeLimit delegate:self];
}
else
{
self.results = [NSMutableArray array];
if(wasEmpty)
{
self.loading = YES;
self.displayController.searchResultsTableView.allowsSelection = NO;
}
else
{
self.loading = NO;
self.displayController.searchResultsTableView.allowsSelection = YES;
}
return YES;
}
return NO;
}
- (void)searchDisplayController:(UISearchDisplayController *)controller willShowSearchResultsTableView:(UITableView *)tableView
{
if([self.viewController isKindOfClass:[UITableViewController class]])
{
[(UITableViewController*)self.viewController tableView].separatorStyle = UITableViewCellSeparatorStyleNone;
}
}
- (void)searchDisplayController:(UISearchDisplayController *)controller willHideSearchResultsTableView:(UITableView *)tableView
{
if([self.viewController isKindOfClass:[UITableViewController class]])
{
[(UITableViewController*)self.viewController tableView].separatorStyle = UITableViewCellSeparatorStyleSingleLine;
}
[self.searcher cancelSearch];
self.searcher = nil;
}
- (void)searcher:(DHDBSearcher *)searcher foundResults:(NSArray *)results hasMore:(BOOL)hasMore
{
if(searcher == self.searcher)
{
NSInteger previousSelection = self.displayController.searchResultsTableView.indexPathForSelectedRow.row;
BOOL isFirst = self.nextResults.count == 0;
self.loading = NO;
self.displayController.searchResultsTableView.allowsSelection = YES;
[self.nextResults addObjectsFromArray:results];
self.results = self.nextResults;
[self.displayController.searchResultsTableView reloadData];
if(isFirst && isRegularHorizontalClass && self.nextResults.count)
{
[self.displayController.searchResultsTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionTop];
DHDBResult *firstResult = self.results[0];
[[DHDBResultSorter sharedSorter] resultWasSelected:firstResult inTableView:self.displayController.searchResultsTableView];
[[DHWebViewController sharedWebViewController] loadResult:firstResult];
}
else if(isRegularHorizontalClass && !isFirst && self.results.count)
{
[self.displayController.searchResultsTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:previousSelection inSection:0] animated:NO scrollPosition:UITableViewScrollPositionNone];
}
if(!hasMore)
{
self.nextResults = nil;
}
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:@"DHNestedSegue"])
{
DHNestedViewController *nestedController = [segue destinationViewController];
DHDBResult *result = self.results[self.displayController.searchResultsTableView.indexPathForSelectedRow.row];
nestedController.result = result;
}
else if([[segue identifier] isEqualToString:@"DHSearchWebViewSegue"])
{
DHWebViewController *webViewController = [segue destinationViewController];
DHDBResult *result = self.results[self.displayController.searchResultsTableView.indexPathForSelectedRow.row];
webViewController.result = result;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(tableView.indexPathForSelectedRow.row < self.results.count)
{
DHDBResult *result = self.results[tableView.indexPathForSelectedRow.row];
if(result.similarResults.count)
{
if(isRegularHorizontalClass)
{
[[DHWebViewController sharedWebViewController] loadResult:[result activeResult]];
}
[self.viewController performSegueWithIdentifier:@"DHNestedSegue" sender:self];
}
else
{
[[DHDBResultSorter sharedSorter] resultWasSelected:result inTableView:tableView];
if(isRegularHorizontalClass)
{
[[DHWebViewController sharedWebViewController] loadResult:result];
}
else
{
[[DHWebViewController sharedWebViewController] loadResult:result];
[self.viewController performSegueWithIdentifier:@"DHSearchWebViewSegue" sender:self];
}
}
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(self.loading)
{
return 3;
}
return self.results.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(self.loading)
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHLoadingCell" forIndexPath:indexPath];
cell.userInteractionEnabled = NO;
if(indexPath.row == 2)
{
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraph setAlignment:NSTextAlignmentCenter];
UIFont *font = [UIFont boldSystemFontOfSize:20];
cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Searching..." attributes:@{NSParagraphStyleAttributeName : paragraph, NSForegroundColorAttributeName: [UIColor colorWithWhite:0.8 alpha:1], NSFontAttributeName: font}];
}
else
{
cell.textLabel.text = @"";
}
return cell;
}
DHBrowserTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHBrowserCell" forIndexPath:indexPath];
DHDBResult *result = (indexPath.row) < self.results.count ? self.results[indexPath.row] : nil;
[cell makeEntryCell];
cell.textLabel.attributedText = nil;
cell.textLabel.font = [UIFont fontWithName:@"Menlo" size:16];
cell.textLabel.text = result.name;
cell.typeImageView.image = result.typeImage;
cell.platformImageView.image = result.platformImage;
[self highlightCell:cell result:result];
[cell.titleLabel setRightDetailText:(result.similarResults.count) ? [NSString stringWithFormat:@"%ld", (unsigned long)result.similarResults.count+1] : @"" adjustMainWidth:YES];
cell.accessoryType = (result.similarResults.count || !isRegularHorizontalClass) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
return cell;
}
- (void)highlightCell:(DHBrowserTableViewCell *)cell result:(DHDBResult *)result
{
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:cell.textLabel.attributedText];
BOOL didAddAttributes = NO;
for(NSString *key in [DHDBResult highlightDictionary])
{
[string removeAttribute:key range:NSMakeRange(0, string.length)];
}
for(NSValue *highlightRangeValue in result.highlightRanges)
{
NSRange highlightRange = [highlightRangeValue rangeValue];
[string addAttributes:[DHDBResult highlightDictionary] range:highlightRange];
didAddAttributes = YES;
}
if(didAddAttributes)
{
cell.textLabel.attributedText = string;
}
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[coder encodeBool:self.displayController.isActive forKey:@"searchIsActive"];
if(self.displayController.isActive)
{
[coder encodeObject:[self.displayController.searchBar text] forKey:@"searchBarText"];
if(self.results)
{
[coder encodeObject:self.results forKey:@"searchResults"];
}
NSIndexPath *selectedIndexPath = [self.displayController.searchResultsTableView indexPathForSelectedRow];
if(selectedIndexPath)
{
[coder encodeObject:selectedIndexPath forKey:@"selectedIndexPath"];
}
BOOL isFirstResponder = [self.displayController.searchBar isFirstResponder];
[coder encodeBool:isFirstResponder forKey:@"isFirstResponder"];
[coder encodeCGPoint:self.displayController.searchResultsTableView.contentOffset forKey:@"scrollPoint"];
}
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
BOOL isActive = [coder decodeBoolForKey:@"searchIsActive"];
if(isActive)
{
self.isRestoring = YES;
self.results = [coder decodeObjectForKey:@"searchResults"];
NSString *searchBarText = [coder decodeObjectForKey:@"searchBarText"];
NSIndexPath *selectedIndexPath = [coder decodeObjectForKey:@"selectedIndexPath"];
BOOL isFirstResponder = [coder decodeBoolForKey:@"isFirstResponder"];
CGPoint scrollPoint = [coder decodeCGPointForKey:@"scrollPoint"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((isRegularHorizontalClass) ? 0.5 * NSEC_PER_SEC : 0)), dispatch_get_main_queue(), ^{
[self.displayController setActive:YES animated:isRegularHorizontalClass];
if(searchBarText)
{
[self.displayController.searchBar setText:searchBarText];
}
if(selectedIndexPath)
{
[self.displayController.searchResultsTableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
if(isFirstResponder)
{
[self.displayController.searchBar becomeFirstResponder];
}
self.displayController.searchResultsTableView.contentOffset = scrollPoint;
self.isRestoring = NO;
});
}
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self.searcher cancelSearch];
}
@end
================================================
FILE: Dash/DHDBSearcher.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@protocol DHDBSearcherDelegate;
@interface DHDBSearcher : NSObject
@property (retain) NSThread *currentThread;
@property (retain) NSString *query;
@property (retain) NSArray *docsets;
@property (weak) id delegate;
@property (retain) NSString *typeLimit;
+ (DHDBSearcher *)searcherWithDocsets:(NSArray *)docsets query:(NSString *)query limitToType:(NSString *)typeLimit delegate:(id)delegate;
- (void)cancelSearch;
+ (void)checkForInterrupt;
@end
@protocol DHDBSearcherDelegate
- (void)searcher:(DHDBSearcher *)searcher foundResults:(NSArray *)results hasMore:(BOOL)hasMore;
@end
================================================
FILE: Dash/DHDBSearcher.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDBSearcher.h"
#import "DHQueuedDB.h"
#import "DHTypes.h"
#import "DHDBNestedResultSorter.h"
@implementation DHDBSearcher
+ (DHDBSearcher *)searcherWithDocsets:(NSArray *)docsets query:(NSString *)query limitToType:(NSString *)typeLimit delegate:(id)delegate
{
DHDBSearcher *searcher = [[DHDBSearcher alloc] init];
searcher.query = [query copy];
searcher.docsets = [NSArray arrayWithArray:docsets];
searcher.typeLimit = typeLimit;
searcher.delegate = delegate;
searcher.currentThread = [[NSThread alloc] initWithTarget:searcher selector:@selector(doSearchThread) object:nil];
[searcher.currentThread setThreadPriority:1.0f];
[searcher.currentThread start];
return searcher;
}
- (void)doSearchThread
{
@autoreleasepool {
NSMutableArray *queue = [NSMutableArray array];
NSMutableArray *fuzzyQueue = [NSMutableArray array];
NSConditionLock *stepLock = [DHDocset stepLock];
@try {
for(DHDocset *docset in self.docsets)
{
DHQueuedDB *db = [DHQueuedDB queueWithDocset:docset query:self.query typeLimit:self.typeLimit isFuzzy:NO];
if(db)
{
[queue addObject:db];
}
DHQueuedDB *fuzzyDB = [DHQueuedDB queueWithDocset:docset query:self.query typeLimit:self.typeLimit isFuzzy:YES];
if(fuzzyDB)
{
[fuzzyQueue addObject:fuzzyDB];
}
}
NSMutableArray *results = nil;
NSMutableArray *perfect = [NSMutableArray array];
NSMutableArray *perfectOriginal = [NSMutableArray array];
NSMutableArray *startsWith = [NSMutableArray array];
NSMutableArray *startsWithOriginal = [NSMutableArray array];
NSMutableArray *endsWith = [NSMutableArray array];
NSMutableArray *endsWithOriginal = [NSMutableArray array];
NSMutableArray *matches = [NSMutableArray array];
NSMutableArray *originalMatches = [NSMutableArray array];
NSMutableArray *fuzzyCamel = [NSMutableArray array];
NSMutableArray *fuzzyPerfect = [NSMutableArray array];
NSMutableArray *fuzzy = [NSMutableArray array];
NSMutableArray *noMatch = [NSMutableArray array];
NSMutableArray *allResults = [NSMutableArray array];
NSMutableSet *duplicates = [NSMutableSet set];
BOOL unifiedPerfectPhase = YES;
BOOL unifiedPrefixPhase = NO;
BOOL unifiedSuffixPhase = NO;
BOOL unifiedContainsPhase = NO;
BOOL fuzzyPhase = NO;
BOOL shouldBreak = NO;
BOOL didSendResultsOnce = NO;
NSInteger count = 0;
NSInteger maxLimit = 200; // you need to change this in DHUnifiedQueuedDB as well
NSMutableArray *processingQueues = [NSMutableArray arrayWithArray:queue];
while(YES)
{
if((unifiedPerfectPhase || unifiedPrefixPhase || unifiedSuffixPhase || (unifiedContainsPhase && count < maxLimit) || (fuzzyPhase)) && processingQueues.count)
{
NSMutableArray *stepQueue = [NSMutableArray arrayWithArray:processingQueues];
for(int i=0; i < stepQueue.count; i++)
{
[DHDBSearcher checkForInterrupt];
DHQueuedDB *queuedDB = stepQueue[i];
if(![queuedDB step])
{
NSInteger index = [processingQueues indexOfObjectIdenticalTo:queuedDB];
if(index != NSNotFound)
{
[processingQueues removeObjectAtIndex:index];
}
[stepQueue removeObjectAtIndex:i];
--i;
}
}
NSInteger innerCount = 0;
while((unifiedPerfectPhase || (unifiedSuffixPhase && count <= maxLimit*2) || (unifiedPrefixPhase && count <= maxLimit) || (unifiedContainsPhase && count <= maxLimit) || (fuzzyPhase && innerCount <= maxLimit/2)) && stepQueue.count)
{
for(int i=0; i < stepQueue.count; i++)
{
DHQueuedDB *queuedDB = stepQueue[i];
if([queuedDB next])
{
DHDBResult *result = [queuedDB currentDBResult];
if(fuzzyPhase && result.whitespaceMatch)
{
result = nil;
}
NSString *duplicateHash = [result duplicateHash];
if(duplicateHash)
{
if([duplicates containsObject:duplicateHash])
{
continue;
}
[duplicates addObject:duplicateHash];
}
[DHDBSearcher checkForInterrupt];
if(result)
{
[queuedDB addResultToResultDictionary:result];
++count;
++innerCount;
}
}
else
{
[stepQueue removeObjectAtIndex:i];
[DHDBSearcher checkForInterrupt];
--i;
}
}
}
}
else
{
shouldBreak = YES;
}
if((fuzzyPhase && shouldBreak) || unifiedContainsPhase)
{
for(DHQueuedDB *queueDB in queue)
{
[queueDB sortResultDictionary];
}
NSArray *types = [[DHTypes sharedTypes] orderedTypes];
if(self.typeLimit)
{
types = @[self.typeLimit];
}
NSArray *fuzzyCamelBackUp = [NSArray arrayWithArray:fuzzyCamel];
NSArray *fuzzyPerfectBackUp = [NSArray arrayWithArray:fuzzyPerfect];
NSArray *fuzzyBackUp = [NSArray arrayWithArray:fuzzy];
NSArray *noMatchBackUp = [NSArray arrayWithArray:noMatch];
[fuzzyCamel removeAllObjects];
[fuzzyPerfect removeAllObjects];
[fuzzy removeAllObjects];
[noMatch removeAllObjects];
// Step 3: Order results based on priority and order similar results
for(NSString *type in types)
{
[DHDBSearcher checkForInterrupt];
for(DHQueuedDB *queueDB in queue)
{
for(DHDBResult *result in [queueDB resultsForType:type])
{
[DHDBSearcher checkForInterrupt];
NSInteger similar = [allResults indexOfObject:result];
if(similar != NSNotFound)
{
if(!result.isSO)
{
[[allResults[similar] similarResults] addObject:result];
}
}
else
{
if(result.perfectMatch)
{
[perfect addObject:result];
}
else if(result.perfectMatchOriginal)
{
[perfectOriginal addObject:result];
}
else if(result.queryIsPrefix)
{
[startsWith addObject:result];
}
else if(result.queryIsSuffix)
{
[endsWith addObject:result];
}
else if(result.matchesQueryAtAll)
{
[matches addObject:result];
}
else if(result.queryIsPrefixOfOriginal)
{
[startsWithOriginal addObject:result];
}
else if(result.queryIsSuffixOfOriginal)
{
[endsWithOriginal addObject:result];
}
else if(result.originalMatchesQueryAtAll)
{
[originalMatches addObject:result];
}
else if(result.fuzzyCamel)
{
[fuzzyCamel addObject:result];
}
else if(result.fuzzyPerfect)
{
[fuzzyPerfect addObject:result];
}
else if(result.fuzzy)
{
[fuzzy addObject:result];
}
else
{
[noMatch addObject:result];
}
[allResults addObject:result];
[result setIsActive:YES];
}
}
}
}
for(DHQueuedDB *queueDB in queue)
{
[queueDB setResultDictionary:[NSMutableDictionary dictionary]];
}
[fuzzyCamel addObjectsFromArray:fuzzyCamelBackUp];
[fuzzyPerfect addObjectsFromArray:fuzzyPerfectBackUp];
[fuzzy addObjectsFromArray:fuzzyBackUp];
[noMatch addObjectsFromArray:noMatchBackUp];
[DHDBSearcher checkForInterrupt];
results = [NSMutableArray array];
BOOL didSubmitFuzzies = NO;
if(unifiedContainsPhase)
{
[results addObjectsFromArray:perfect];
[results addObjectsFromArray:perfectOriginal];
[results addObjectsFromArray:startsWith];
[results addObjectsFromArray:endsWith];
[results addObjectsFromArray:matches];
[results addObjectsFromArray:startsWithOriginal];
[results addObjectsFromArray:endsWithOriginal];
[results addObjectsFromArray:originalMatches];
[perfect removeAllObjects];
[perfectOriginal removeAllObjects];
[startsWith removeAllObjects];
[endsWith removeAllObjects];
[matches removeAllObjects];
[startsWithOriginal removeAllObjects];
[endsWithOriginal removeAllObjects];
[originalMatches removeAllObjects];
}
if((fuzzyPhase && shouldBreak) || (count >= maxLimit && unifiedContainsPhase))
{
[results addObjectsFromArray:fuzzyCamel];
[results addObjectsFromArray:fuzzyPerfect];
[results addObjectsFromArray:fuzzy];
[results addObjectsFromArray:noMatch];
[fuzzyCamel removeAllObjects];
[fuzzyPerfect removeAllObjects];
[fuzzy removeAllObjects];
[noMatch removeAllObjects];
didSubmitFuzzies = YES;
}
for(int i = 0; i < results.count; i++)
{
DHDBResult *result = results[i];
if(result.similarResults.count)
{
results[i] = [[DHDBNestedResultSorter sharedSorter] sortNestedResults:result];
}
}
[DHDBSearcher checkForInterrupt];
if(results.count || didSubmitFuzzies || shouldBreak)
{
[DHDBSearcher checkForInterrupt];
if(results.count || (!didSendResultsOnce && (didSubmitFuzzies || shouldBreak)))
{
[DHDBSearcher checkForInterrupt];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.delegate searcher:self foundResults:[NSArray arrayWithArray:results] hasMore:!(didSubmitFuzzies || shouldBreak)];
});
[DHDBSearcher checkForInterrupt];
[DHDBSearcher checkForInterrupt];
}
didSendResultsOnce = results.count > 0;
if(didSubmitFuzzies || shouldBreak)
{
break;
}
[results removeAllObjects];
}
}
if(unifiedPerfectPhase)
{
unifiedPerfectPhase = NO;
unifiedPrefixPhase = YES;
}
else if(unifiedPrefixPhase)
{
unifiedPrefixPhase = NO;
unifiedSuffixPhase = YES;
}
else if(unifiedSuffixPhase)
{
unifiedSuffixPhase = NO;
unifiedContainsPhase = YES;
}
if(!processingQueues.count && unifiedContainsPhase && count <= maxLimit)
{
unifiedContainsPhase = NO;
fuzzyPhase = YES;
[processingQueues addObjectsFromArray:fuzzyQueue];
[queue removeAllObjects];
[queue addObjectsFromArray:fuzzyQueue];
}
}
[stepLock lock];
[stepLock unlockWithCondition:DHLockAllAllowed];
}
@catch(NSException *exception) {
[stepLock lock];
[stepLock unlockWithCondition:DHLockAllAllowed];
if(![[exception name] isEqualToString:@"Interrupt"])
{
NSLog(@"FIXME: exception in doSearchThread: %@", exception);
NSLog(@"%@", [NSThread callStackSymbols]);
[self.delegate searcher:self foundResults:@[] hasMore:NO];
}
}
}
}
- (void)cancelSearch
{
[self.currentThread cancel];
[self.currentThread setThreadPriority:0.00f];
self.delegate = nil;
self.currentThread = nil;
}
+ (void)checkForInterrupt
{
if([[NSThread currentThread] isCancelled])
{
[NSException raise:@"Interrupt" format:@""];
}
}
- (void)dealloc
{
[self cancelSearch];
}
@end
================================================
FILE: Dash/DHDocset.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
@interface DHDocset : NSObject
// If you add anything that gets changed by the user (e.g. custom keyword),
// you should also add it to grabUserDataFromDocset: as well to make it
// persist when the docset gets replaced during updates
@property (strong) NSString *relativePath;
@property (strong) NSString *tempOptimisedIndexPath;
@property (strong) NSString *name;
@property (strong) NSString *bundleIdentifier;
@property (strong) NSString *platform;
@property (strong) NSString *parseFamily;
@property (strong) NSString *nameShorteningFamily;
@property (strong) NSString *declaredInStyle;
@property (assign) BOOL isJavaScriptEnabled;
@property (assign) BOOL blocksOnlineResources;
@property (assign) BOOL isDashDocset;
@property (assign) BOOL isEnabled;
@property (strong) NSString *_indexFilePath;
@property (strong) NSString *pluginKeyword;
@property (strong) NSString *suggestedKeyword;
@property (strong) NSNumber *version;
@property (strong) UIImage *_icon;
@property (strong) NSString *_path;
@property (strong) NSNumber *hasCustomIcon;
@property (strong) NSString *repoIdentifier;
@property (strong) NSString *feedIdentifier;
- (void)grabUserDataFromDocset:(DHDocset *)docset;
+ (DHDocset *)docsetAtPath:(NSString *)path;
+ (DHDocset *)docsetWithDictionaryRepresentation:(NSDictionary *)dictionary;
- (NSDictionary *)dictionaryRepresentation;
+ (DHDocset *)firstDocsetInsideFolder:(NSString *)path;
+ (NSConditionLock *)stepLock;
- (void)executeBlockWithinDocsetDBConnection:(void (^)(FMDatabase *db))block readOnly:(BOOL)readOnly lockCondition:(int)lockCondition optimisedIndex:(BOOL)optimisedIndex;
- (NSString *)documentsPath;
- (NSString *)resourcesPath;
- (NSString *)contentsPath;
- (NSString *)tarixPath;
- (NSString *)tarixIndexPath;
- (UIImage *)icon;
- (UIImage *)grabIcon;
- (NSString *)path;
- (NSDictionary *)plist;
- (NSString *)sqlPath;
- (NSString *)optimisedIndexPath;
- (NSString *)indexFilePath;
#define DHLockAllAllowed 3
#define DHLockSearchOnly 2
#define DHLockDontLock -1
@end
================================================
FILE: Dash/DHDocset.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocset.h"
@implementation DHDocset
@synthesize _icon = _iconCache;
@synthesize _path = _pathCache;
static NSConditionLock *_stepLock = nil;
- (void)grabUserDataFromDocset:(DHDocset *)docset
{
self.isEnabled = docset.isEnabled;
}
+ (DHDocset *)docsetWithDictionaryRepresentation:(NSDictionary *)dictionary
{
DHDocset *docset = [[DHDocset alloc] init];
docset.relativePath = dictionary[@"relativePath"];
docset.name = dictionary[@"name"];
docset.bundleIdentifier = dictionary[@"bundleIdentifier"];
docset.platform = dictionary[@"platform"];
docset.parseFamily = dictionary[@"parseFamily"];
docset.nameShorteningFamily = dictionary[@"nameShorteningFamily"];
docset.declaredInStyle = dictionary[@"declaredInStyle"];
docset.isJavaScriptEnabled = [dictionary[@"isJavaScriptEnabled"] boolValue];
docset.isEnabled = [dictionary[@"isEnabled"] boolValue];
docset.blocksOnlineResources = [dictionary[@"blocksOnlineResources"] boolValue];
docset.isDashDocset = [dictionary[@"isDashDocset"] boolValue];
docset._indexFilePath = dictionary[@"indexFilePath"];
docset.pluginKeyword = dictionary[@"pluginKeyword"];
docset.suggestedKeyword = dictionary[@"suggestedKeyword"];
docset.version = dictionary[@"version"];
docset.hasCustomIcon = dictionary[@"hasCustomIcon"];
docset.feedIdentifier = dictionary[@"feedIdentifier"];
docset.repoIdentifier = dictionary[@"repoIdentifier"];
return docset;
}
- (NSDictionary *)dictionaryRepresentation
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
if(self.relativePath) { dictionary[@"relativePath"] = self.relativePath; }
if(self.name) { dictionary[@"name"] = self.name; }
if(self.bundleIdentifier) { dictionary[@"bundleIdentifier"] = self.bundleIdentifier; }
if(self.platform) { dictionary[@"platform"] = self.platform; }
if(self.parseFamily) { dictionary[@"parseFamily"] = self.parseFamily; }
if(self.nameShorteningFamily) { dictionary[@"nameShorteningFamily"] = self.nameShorteningFamily; }
if(self.declaredInStyle) { dictionary[@"declaredInStyle"] = self.declaredInStyle; }
dictionary[@"isJavaScriptEnabled"] = @(self.isJavaScriptEnabled);
dictionary[@"blocksOnlineResources"] = @(self.blocksOnlineResources);
dictionary[@"isDashDocset"] = @(self.isDashDocset);
dictionary[@"isEnabled"] = @(self.isEnabled);
if(self._indexFilePath) { dictionary[@"indexFilePath"] = self._indexFilePath; }
if(self.pluginKeyword) { dictionary[@"pluginKeyword"] = self.pluginKeyword; }
if(self.suggestedKeyword) { dictionary[@"suggestedKeyword"] = self.suggestedKeyword; }
if(self.version) { dictionary[@"version"] = self.version; }
if(self.hasCustomIcon) { dictionary[@"hasCustomIcon"] = self.hasCustomIcon; }
if(self.feedIdentifier) { dictionary[@"feedIdentifier"] = self.feedIdentifier; }
if(self.repoIdentifier) { dictionary[@"repoIdentifier"] = self.repoIdentifier; }
return dictionary;
}
- (NSDictionary *)plist
{
return [NSDictionary dictionaryWithContentsOfFile:[self.path stringByAppendingPathComponent:@"Contents/Info.plist"]];
}
+ (DHDocset *)docsetAtPath:(NSString *)path
{
DHDocset *docset = [[DHDocset alloc] init];
docset.relativePath = [path substringFromString:[homePath stringByDeletingLastPathComponent]];
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:[path stringByAppendingPathComponent:@"Contents/Info.plist"]];
if(!plist)
{
return nil;
}
NSString *bundle = plist[@"CFBundleIdentifier"];
if(!bundle)
{
bundle = @"";
}
docset.bundleIdentifier = bundle;
NSString *platform = plist[@"DocSetPlatformFamily"];
if([platform isEqualToString:@"macosx"])
{
platform = @"osx";
}
else if([platform isEqualToString:@"iphoneos"])
{
platform = @"ios";
}
else if([platform isEqualToString:@"appletvos"])
{
platform = @"tvos";
}
docset.platform = platform ? platform : @"unknown";
NSString *parseFamily = plist[@"DashDocSetFamily"];
if(parseFamily)
{
docset.parseFamily = parseFamily;
}
NSString *nameShorteningFamily = plist[@"DashDocSetNameShorteningFamily"];
if(nameShorteningFamily)
{
docset.nameShorteningFamily = nameShorteningFamily;
}
NSString *declaredInStyle = plist[@"DashDocSetDeclaredInStyle"];
if(declaredInStyle)
{
docset.declaredInStyle = declaredInStyle;
}
NSNumber *isJavaScriptEnabled = plist[@"isJavaScriptEnabled"];
BOOL isPlatformJavaScriptEnabled = [platform isEqualToString:@"doxy"] || [parseFamily isEqualToString:@"doxy"] || [platform isEqualToString:@"doxygen"] || [parseFamily isEqualToString:@"doxygen"];
BOOL isJSEnabled = [isJavaScriptEnabled boolValue] || isPlatformJavaScriptEnabled;
docset.isJavaScriptEnabled = isJSEnabled;
NSNumber *blocksOnlineResources = plist[@"DashDocSetBlocksOnlineResources"];
if(blocksOnlineResources)
{
docset.blocksOnlineResources = [blocksOnlineResources boolValue];
}
NSString *indexFilePath = plist[@"dashIndexFilePath"];
if(indexFilePath.length)
{
docset._indexFilePath = indexFilePath;
}
NSString *pluginKeyword = plist[@"DashDocSetPluginKeyword"];
if(pluginKeyword)
{
docset.pluginKeyword = pluginKeyword;
}
NSString *suggestedKeyword = plist[@"DashDocSetKeyword"];
if(suggestedKeyword)
{
docset.suggestedKeyword = suggestedKeyword;
}
NSString *versionString = plist[@"DocSetPlatformVersion"];
versionString = (versionString) ? versionString : plist[@"CFBundleVersion"];
if(versionString)
{
NSNumber *version = @([versionString doubleValue]);
if(version)
{
docset.version = version;
}
else
{
docset.version = @1.0f;
}
}
else
{
docset.version = @1.0f;
}
NSString *name = plist[@"CFBundleName"];
name = (name == nil) ? @"Unknown" : name;
if([name hasSuffix:@" doc set"])
{
name = [name substringToLastOccurrenceOfString:@" doc set"];
}
else if([name hasSuffix:@" Documentation"])
{
name = [name substringToLastOccurrenceOfString:@" Documentation"];
}
if([platform isEqualToString:@"ios"] || [platform isEqualToString:@"osx"] || [platform isEqualToString:@"watchos"] || [platform isEqualToString:@"tvos"])
{
if([name hasCaseInsensitiveSuffix:@" library"])
{
name = [name substringToLastOccurrenceOfString:@" library"];
name = [name substringToLastOccurrenceOfString:@" Library"];
}
name = [name stringByReplacingOccurrencesOfString:@"OS X v" withString:@"OS X "];
}
if([bundle isEqualToString:@"com.apple.adc.documentation"])
{
name = @"Apple Guides and Sample Code";
docset.suggestedKeyword = @"apple";
}
docset.name = name;
NSNumber *isDashNumber = plist[@"isDashDocset"];
BOOL isDash = (isDashNumber) ? [isDashNumber boolValue] : NO;
docset.isDashDocset = isDash;
docset.isEnabled = YES;
return docset;
}
+ (DHDocset *)firstDocsetInsideFolder:(NSString *)path
{
if([[path pathExtension] isCaseInsensitiveEqual:@"docset"])
{
return [DHDocset docsetAtPath:path];
}
NSString *docset = [[NSFileManager defaultManager] firstFileWithExtension:@"docset" atPath:path ignoreHidden:YES];
if(docset)
{
return [DHDocset docsetAtPath:[path stringByAppendingPathComponent:docset]];
}
return nil;
}
+ (void)initLock
{
@synchronized([DHDocset class])
{
if(!_stepLock)
{
_stepLock = [[NSConditionLock alloc] initWithCondition:3];
}
}
}
+ (NSConditionLock *)stepLock
{
if(!_stepLock)
{
[DHDocset initLock];
}
return _stepLock;
}
- (void)executeBlockWithinDocsetDBConnection:(void (^)(FMDatabase *db))block readOnly:(BOOL)readOnly lockCondition:(int)lockCondition optimisedIndex:(BOOL)optimisedIndex
{
NSString *docsetSQLPath = (optimisedIndex) ? (self.tempOptimisedIndexPath) ? self.tempOptimisedIndexPath : self.optimisedIndexPath : self.sqlPath;
if(!docsetSQLPath)
{
return;
}
NSConditionLock *lock = (lockCondition != DHLockDontLock) ? [DHDocset stepLock] : nil;
[lock lockWhenCondition:lockCondition];
FMDatabase *db = [FMDatabase databaseWithPath:docsetSQLPath];
if((readOnly) ? [db openWithFlags:SQLITE_OPEN_READONLY] : [db open])
{
if(optimisedIndex)
{
[db registerFTSExtensions];
}
[lock unlockWithCondition:lockCondition];
@try {
block(db);
}
@catch (NSException *exception) {
if([[exception name] isEqualToString:@"Indexing Interrupt"])
{
[NSException raise:@"Indexing Interrupt" format:@""];
}
else
{
NSLog(@"FIXME: Exception in executeBlockWithinDocsetDB: %@", exception);
NSLog(@"%@", [NSThread callStackSymbols]);
}
}
@finally {
[lock lockWhenCondition:lockCondition];
[db close];
[lock unlockWithCondition:DHLockAllAllowed];
}
}
else
{
[lock unlockWithCondition:DHLockAllAllowed];
}
}
- (UIImage *)icon
{
return (self._icon) ? : [self grabIcon];
}
- (UIImage *)grabIcon
{
UIImage *icon = nil;
if(!self.hasCustomIcon || [self.hasCustomIcon boolValue])
{
NSString *iconPath = [self.path stringByAppendingPathComponent:@"icon.png"];
icon = [DHImageCache imageWithContentsOfFile:iconPath fullRefresh:!self.hasCustomIcon];
if(!icon)
{
iconPath = [self.path stringByAppendingPathComponent:@"icon.tiff"];
icon = [DHImageCache imageWithContentsOfFile:iconPath fullRefresh:!self.hasCustomIcon];
}
self.hasCustomIcon = [NSNumber numberWithBool:icon != nil];
}
if([self.bundleIdentifier isEqualToString:@"com.apple.adc.documentation"])
{
icon = [UIImage imageNamed:@"apple"];
}
if(!icon)
{
NSString *platform = (self.platform) ? self.platform : @"Other";
if(self.parseFamily && [self.parseFamily isEqualToString:@"cheatsheet"])
{
platform = @"cheatsheet";
}
if([platform isEqualToString:@"macosx"] || [platform isEqualToString:@"osx"])
{
icon = [UIImage imageNamed:@"Mac"];
}
else if([platform isEqualToString:@"iphoneos"] || [platform isEqualToString:@"ios"])
{
icon = [UIImage imageNamed:@"iphone"];
}
else
{
icon = [UIImage imageNamed:platform];
if(!icon)
{
icon = [UIImage imageNamed:@"Other"];
}
}
}
self._icon = icon;
return icon;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"Docset with name: %@, platform: %@, isDashDocset: %d", self.name, self.platform, self.isDashDocset];
}
- (NSString *)contentsPath
{
return [self.path stringByAppendingPathComponent:@"Contents"];
}
- (NSString *)resourcesPath
{
return [self.path stringByAppendingPathComponent:@"Contents/Resources"];
}
- (NSString *)documentsPath
{
return [self.path stringByAppendingPathComponent:@"Contents/Resources/Documents"];
}
- (NSString *)tarixPath
{
return [self.path stringByAppendingPathComponent:@"Contents/Resources/tarix.tgz"];
}
- (NSString *)tarixIndexPath
{
return [self.path stringByAppendingPathComponent:@"Contents/Resources/tarixIndex.db"];
}
- (BOOL)isEqual:(DHDocset *)object
{
return [self.path isCaseInsensitiveEqual:[object path]];
}
- (NSString *)path
{
if(!self._path)
{
self._path = [[homePath stringByDeletingLastPathComponent] stringByAppendingPathComponent:self.relativePath];
}
return self._path;
}
- (NSString *)sqlPath
{
return [self.path stringByAppendingPathComponent:@"Contents/Resources/docSet.dsidx"];
}
- (NSString *)optimisedIndexPath
{
return [self.path stringByAppendingPathComponent:@"Contents/Resources/optimisedIndex.dsidx"];
}
- (NSString *)indexFilePath
{
NSString *indexFile = self._indexFilePath;
NSString *docsetPath = self.documentsPath;
NSString *platform = self.platform;
NSString *parseFamily = self.parseFamily;
NSFileManager *fileManager = [NSFileManager defaultManager];
if(indexFile)
{
if(indexFile.length > 0)
{
if([indexFile hasPrefix:@"http://"] || [indexFile hasPrefix:@"https://"] || [indexFile hasPrefix:@"dash-apple-api://"])
{
return indexFile;
}
if([platform isEqualToString:@"apple"] && [DHAppleActiveLanguage currentLanguage] == DHNewActiveAppleLanguageObjC)
{
indexFile = [indexFile stringByReplacingOccurrencesOfString:@".html" withString:@"1742.html"];
}
NSString *fullPath = [docsetPath stringByAppendingPathComponent:[indexFile substringToString:@"#"]];
if([fileManager fileExistsAtPathOrInIndex:fullPath])
{
if([indexFile contains:@"#"])
{
fullPath = [fullPath stringByAppendingFormat:@"#%@", [indexFile substringFromString:@"#"]];
}
return fullPath;
}
}
}
NSMutableArray *pathsToTry = [NSMutableArray arrayWithObjects:@"dash-browse-index.html", @"prototypejs/index.html", @"sqlite/index.html", @"documentation/Cocoa/Reference/Foundation/ObjC_classic/_index.html", @"documentation/ToolsLanguages/Conceptual/Xcode_User_Guide/000-About_Xcode/about.html", @"documentation/IDEs/Conceptual/xcode_quick_start/index.html", @"package-detail.html", @"docs/reference/packages.html", @"Arduino/index.html", @"output/en.cppreference.com/w/cpp.html", @"output/en/cpp.html", @"clojure/api-index.html", @"developer.anscamobile.com/reference/index.html", @"docs.coronalabs.com/api/index.html", @"developer.mozilla.org/en/CSS/CSS_Reference.html", @"developer.mozilla.org/en-US/docs/CSS/CSS_Reference.html", @"api/overview-summary.html", @"haskell/index.html", @"developer.mozilla.org/en/HTML/HTML5.html", @"developer.mozilla.org/en/JavaScript/Reference.html", @"developer.mozilla.org/en/JavaScript/Reference.html", @"www.lua.org/manual/5.2/index.html", @"www.lua.org/manual/5.1/index.html", @"www.lua.org/manual/5.3/index.html", @"nodejs/api/documentation.html", @"nodejs/api/api/documentation.html", @"perldoc-html/index-functions-by-cat.html", @"res/index.html", @"genindex-all.html", @"topics/introduction.html", @"introduction.html", @"api.rubyonrails.org/files/RDOC_MAIN_rdoc.html", @"scala/package.html", @"akka/package.html", @"docs/welcome.html", @"developer.mozilla.org/en/XSLT/Elements.html", @"developer.mozilla.org/en/XUL_Reference.html", @"genindex.html", @"html/classes.html", @"html/qtdoc/classes.html", @"api.jquery.com/index.html", @"helphelp.html", @"partials/guide/index.html", @"elisp/index.html", @"docs/right-pane.html", @"doc/man_index.html", @"docs.go-mono.com/monoroot.html", @"api/index.html", @"mongo/genindex.html", @"HyperSpec/HyperSpec/Front/index.htm", @"api.jqueryui.com/category/all/index.html", @"golang.org/ref/index.html", @"documentation/ToolsLanguages/Conceptual/Xcode_Overview/About_Xcode/about.html", @"documentation/Cocoa/Reference/Foundation/ObjC_classic/index.html", @"documentation/ToolsLanguages/Conceptual/Xcode_Overview/index.html", nil];
if([platform isEqualToString:@"c"])
{
[pathsToTry removeObject:@"output/en/cpp.html"];
[pathsToTry removeObject:@"output/en.cppreference.com/w/cpp.html"];
[pathsToTry addObject:@"output/en/c.html"];
[pathsToTry addObject:@"output/en.cppreference.com/w/c.html"];
}
NSArray *firstIndexPlatforms = @[@"cappuccino", @"cocos2dx", @"underscore", @"backbone", @"coffee", @"appledoc", @"doxy", @"doxygen", @"gl2", @"gl3", @"gl4", @"sparrow", @"cocos2d", @"codeigniter", @"django", @"joomla", @"symfony", @"kobold2d", @"mysql", @"psql", @"typo3", @"twisted", @"zend", @"glib"];
if([firstIndexPlatforms containsObject:platform] || (parseFamily && [firstIndexPlatforms containsObject:parseFamily]))
{
[pathsToTry insertObject:@"index.html" atIndex:0];
}
for(NSString *path in pathsToTry)
{
NSString *dashIndexPath = [docsetPath stringByAppendingPathComponent:path];
if([fileManager fileExistsAtPathOrInIndex:dashIndexPath])
{
self._indexFilePath = path;
return dashIndexPath;
}
}
self._indexFilePath = @"";
return [[NSBundle mainBundle] pathForResource:@"home" ofType:@"html"];
}
@end
================================================
FILE: Dash/DHDocsetBrowser.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHWebViewController.h"
#import "DHBrowserTableViewCell.h"
#import "DHDBSearchController.h"
#import "DHBrowserTableView.h"
@interface DHDocsetBrowser : UITableViewController
@property (assign) BOOL didFirstReload;
@property (strong) DHDBSearchController *searchController;
@property (strong, readonly) NSArray *shownDocsets;
@property (assign) BOOL didLoad;
@property (assign) BOOL isSearching;
@property (assign) BOOL needsToReloadWhenDoneSearching;
- (IBAction)openSettings:(id)sender;
+ (NSAttributedString *)titleBarItemAttributedStringTemplate;
@end
================================================
FILE: Dash/DHDocsetBrowser.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetBrowser.h"
#import "DHWebViewController.h"
#import "DHPreferences.h"
#import "DHNavigationAnimator.h"
#import "DHRepo.h"
#import "DHDocsetManager.h"
#import "DHAppDelegate.h"
#import "DHDocsetDownloader.h"
#import "DHRemoteBrowser.h"
#import "DHWebView.h"
#import "DHDocsetBrowserViewModel.h"
static NSAttributedString *_titleBarItemAttributedStringTemplate = nil;
@interface DHDocsetBrowser ()
@property (nonatomic, strong) DHDocsetBrowserViewModel *viewModel;
@end
@implementation DHDocsetBrowser
- (NSArray *)shownDocsets {
return self.viewModel.shownDocsets;
}
- (NSArray *)sections {
return self.viewModel.sections;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.viewModel = [[DHDocsetBrowserViewModel alloc] init];
self.clearsSelectionOnViewWillAppear = NO;
self.searchController = [DHDBSearchController searchControllerWithDocsets:nil typeLimit:nil viewController:self];
[self.tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload:) name:DHDocsetsChangedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload:) name:DHRemotesChangedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload:) name:DHSettingsChangedNotification object:nil];
self.tableView.rowHeight = 44;
self.navigationController.delegate = self;
self.navigationController.interactivePopGestureRecognizer.delegate = (id)self;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(performURLSearch:) name:DHPerformURLSearch object:nil];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.tableView.emptyDataSetSource = self;
self.tableView.emptyDataSetDelegate = self;
if(!self.didFirstReload)
{
self.didFirstReload = YES;
[self reload:nil];
}
[self.searchController viewDidAppear];
[self grabTitleBarItemAttributedStringTemplate];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.searchController viewDidDisappear];
}
- (void)viewWillAppear:(BOOL)animated
{
[self.navigationItem.leftBarButtonItem setEnabled:YES];
[super viewWillAppear:animated];
if(!self.isEditing)
{
[self.tableView deselectAll:YES];
}
[self.searchController viewWillAppear];
if([DHRemoteServer sharedServer].connectedRemote)
{
[UIApplication sharedApplication].idleTimerDisabled = NO;
[[DHRemoteServer sharedServer].connectedRemote disconnect];
if(isRegularHorizontalClass)
{
DHWebViewController *webViewController = [DHWebViewController sharedWebViewController];
[webViewController loadURL:[[NSBundle mainBundle] pathForResource:@"home" ofType:@"html"]];
[(DHWebView*)webViewController.webView resetHistory];
[webViewController updateBackForwardButtonState];
webViewController.webView.scrollView.delegate = webViewController;
webViewController.navigationController.toolbarHidden = NO;
}
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.searchController viewWillDisappear];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
[self.searchController traitCollectionDidChange:previousTraitCollection];
}
- (void)performURLSearch:(NSNotification *)notification
{
[self.navigationController popToRootViewControllerAnimated:NO];
NSString *url = [notification object];
NSString *query = nil;
NSMutableOrderedSet *keywordDocsets = [NSMutableOrderedSet orderedSet];
if([url hasCaseInsensitivePrefix:@"dash://"])
{
query = [url substringFromIndex:@"dash://".length];
}
else if([url hasCaseInsensitivePrefix:@"dash-plugin://"])
{
NSString *all = [[url stringByReplacingOccurrencesOfString:@"dash-plugin://" withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, url.length)] trimWhitespace];
query = ([all contains:@"query="]) ? [[all substringFromString:@"query="] substringToString:@"&"] : @"";
NSString *keysString = ([all contains:@"keys="]) ? [[all substringFromString:@"keys="] substringToString:@"&"] : @"";
if(keysString.length)
{
NSMutableArray *docsets = [NSMutableArray arrayWithArray:[DHDocsetManager sharedManager].docsets];
NSArray *keys = [keysString componentsSeparatedByString:@","];
for(__strong NSString *key in keys)
{
key = [[key stringByReplacingPercentEscapes] trimWhitespace];
BOOL isExact = [key hasPrefix:@"exact:"];
if(isExact)
{
key = [key substringFromIndex:@"exact:".length];
}
if(key.length)
{
NSMutableArray *aliasKeys = [NSMutableArray arrayWithObject:key];
if([key isCaseInsensitiveEqual:@"macosx"])
{
[aliasKeys addObject:@"osx"];
}
else if([key isCaseInsensitiveEqual:@"osx"])
{
[aliasKeys addObject:@"macosx"];
}
else if([key isCaseInsensitiveEqual:@"ios"])
{
[aliasKeys addObject:@"iphoneos"];
}
else if([key isCaseInsensitiveEqual:@"iphoneos"])
{
[aliasKeys addObject:@"ios"];
}
for(NSString *aliasKey in aliasKeys)
{
for(DHDocset *docset in docsets)
{
NSString *platform = [[[docset.platform stringByReplacingOccurrencesOfString:@" " withString:@""] lowercaseString] stringByReplacingOccurrencesOfString:@":" withString:@""];
NSString *parseFamily = [[[docset.parseFamily stringByReplacingOccurrencesOfString:@" " withString:@""] lowercaseString] stringByReplacingOccurrencesOfString:@":" withString:@""];
NSString *pluginKeyword = docset.pluginKeyword;
if((platform && platform.length && [platform isCaseInsensitiveEqual:aliasKey]) || (!isExact && parseFamily && parseFamily.length && [parseFamily isCaseInsensitiveEqual:aliasKey]) || (!isExact && pluginKeyword && pluginKeyword.length && [pluginKeyword isCaseInsensitiveEqual:aliasKey]))
{
[keywordDocsets addObject:docset];
}
}
}
}
}
}
}
query = [[query stringByReplacingPercentEscapes] trimWhitespace];
self.searchDisplayController.searchBar.text = @"";
[self.searchDisplayController setActive:NO animated:NO];
[self.searchDisplayController.searchBar resignFirstResponder];
if((query && query.length) || keywordDocsets.count)
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.00 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if(keywordDocsets.count)
{
self.viewModel.keyDocsets = [NSMutableArray arrayWithArray:[keywordDocsets array]];
}
[self.searchDisplayController setActive:YES animated:NO];
self.searchDisplayController.searchBar.text = query;
if(!query.length)
{
[self.searchDisplayController.searchBar becomeFirstResponder];
}
});
}
}
- (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView
{
NSString *text = @"No Docsets";
return [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue-Light" size:22.0], NSForegroundColorAttributeName: [UIColor colorWithWhite:201.0/255.0 alpha:1.0]}];
}
- (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView {
NSString *text = @"You can download some in Settings.";
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
paragraph.lineBreakMode = NSLineBreakByWordWrapping;
paragraph.alignment = NSTextAlignmentCenter;
paragraph.lineSpacing = 4.0;
NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:13.0], NSForegroundColorAttributeName: [UIColor colorWithWhite:207.0/255.0 alpha:1.0], NSParagraphStyleAttributeName: paragraph};
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}
- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView
{
return [UIImage imageNamed:@"placeholder_docsets"];
}
- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state
{
NSString *text = @"Open Settings";
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16.0], NSForegroundColorAttributeName: [[DHAppDelegate sharedDelegate].window.rootViewController.view.tintColor colorWithAlphaComponent:state == UIControlStateNormal ? 0.7 : 0.2]};
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}
- (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView
{
return NO;
}
- (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView
{
return 24;
}
- (void)emptyDataSetDidTapButton:(UIScrollView *)scrollView
{
[self openSettings:self];
}
- (void)reload:(NSNotification *)notification
{
if(self.isSearching)
{
if(!notification || [[notification name] isEqualToString:DHDocsetsChangedNotification])
{
self.needsToReloadWhenDoneSearching = YES;
}
return;
}
NSArray *selected = (self.isEditing) ? [self.tableView indexPathsForSelectedRows] : nil;
[self updateSections:YES];
self.navigationItem.rightBarButtonItem = ([DHDocsetManager sharedManager].docsets.count > 0) ? self.editButtonItem : nil;
self.tableView.tableFooterView = (self.sections.count) ? nil : [UIView new];
[self.tableView reloadData];
for(NSIndexPath *toSelect in selected)
{
[self.tableView selectRowAtIndexPath:toSelect animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
[self updateSections:YES];
return self.sections.count;
}
- (void)updateSections:(BOOL)withTitleUpdate
{
[self.viewModel updateSectionsForEditing:self.isEditing andSearching:self.isSearching];
if(withTitleUpdate)
{
[self updateTitle];
}
}
- (void)updateTitle
{
if([DHRemoteServer sharedServer].remotes.count && !self.isEditing)
{
self.navigationItem.title = (self.sections.count > 1 || [DHDocsetManager sharedManager].docsets.count) ? @"Docsets & Remotes" : @"Remotes";
}
else
{
self.navigationItem.title = @"Docsets";
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.sections[section] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if(self.isEditing)
{
return ([DHRemoteServer sharedServer].remotes.count) ? @"Docsets" : nil;
}
if(self.sections.count > 1 || ([DHDocsetManager sharedManager].docsets.count && !self.shownDocsets.count))
{
return (section == 1 && self.shownDocsets.count) ? @"Docsets" : @"Remotes";
}
return nil;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DHBrowserTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHBrowserCell" forIndexPath:indexPath];
DHDocset *docset = self.sections[indexPath.section][indexPath.row];
cell.textLabel.text = docset.name;
cell.detailTextLabel.text = @"";
[cell.titleLabel setSubtitle:([docset isKindOfClass:[DHRemote class]]) ? @"Not Connected" : @""];
cell.imageView.image = docset.icon;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
[[DHDocsetManager sharedManager] moveDocsetAtIndex:fromIndexPath.row toIndex:toIndexPath.row];
}
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return self.viewModel.canMoveRows;
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 3;
}
- (BOOL)tableView:(UITableView *)tableview shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(self.editing)
{
[self.shownDocsets[indexPath.row] setIsEnabled:YES];
[[DHDocsetManager sharedManager] saveDefaults];
return;
}
if([self.sections[indexPath.section][indexPath.row] isKindOfClass:[DHRemote class]])
{
[self performSegueWithIdentifier:@"DHRemoteBrowserSegue" sender:self];
}
else
{
[self performSegueWithIdentifier:@"DHTypeBrowserSegue" sender:self];
}
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(self.editing)
{
[self.shownDocsets[indexPath.row] setIsEnabled:NO];
[[DHDocsetManager sharedManager] saveDefaults];
return;
}
}
- (void)tableViewDidBeginEditing:(UITableView *)tableView
{
BOOL remotesWereShown = [DHRemoteServer sharedServer].remotes.count > 0;
BOOL docsetsWereShown = (!remotesWereShown && self.sections.count) || (remotesWereShown && self.sections.count > 1);
if(docsetsWereShown)
{
[self.tableView selectRowsInIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self.tableView numberOfRowsInSection:self.sections.count-1])] inSection:self.sections.count-1 animated:NO scrollPosition:UITableViewScrollPositionNone];
}
// NSArray *current = self.shownDocsets;
NSArray *new = [self.viewModel docsetsForEditing:YES];
self.viewModel.shownDocsets = new;
NSMutableArray *toInsert = [NSMutableArray array];
for(NSInteger i = 0; i < new.count; i++)
{
DHDocset *docset = new[i];
if(!docset.isEnabled)
{
[toInsert addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
}
[self updateSections:YES];
[self.tableView beginUpdates];
if(!docsetsWereShown)
{
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
else
{
[self.tableView insertRowsAtIndexPaths:toInsert withRowAnimation:UITableViewRowAnimationFade];
}
if(remotesWereShown)
{
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
[self.tableView endUpdates];
[self.tableView reloadEmptyDataSet];
self.tableView.tableFooterView = (self.sections.count) ? nil : [UIView new];
}
- (void)tableViewDidEndEditing:(UITableView *)tableView
{
NSArray *current = self.shownDocsets;
self.viewModel.shownDocsets = [self.viewModel docsetsForEditing:NO];
NSMutableArray *toDelete = [NSMutableArray array];
BOOL docsetsShouldBeShown = NO;
for(int i = 0; i < current.count; i++)
{
if(![current[i] isEnabled])
{
[toDelete addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
else
{
docsetsShouldBeShown = YES;
}
}
BOOL remotesShouldBeShown = [DHRemoteServer sharedServer].remotes.count > 0;
[self updateSections:YES];
[self.tableView beginUpdates];
if(docsetsShouldBeShown)
{
[self.tableView deleteRowsAtIndexPaths:toDelete withRowAnimation:UITableViewRowAnimationFade];
}
else
{
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
if(remotesShouldBeShown)
{
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
[self.tableView endUpdates];
[self.tableView reloadEmptyDataSet];
self.tableView.tableFooterView = (self.sections.count) ? nil : [UIView new];
}
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
if(self.viewModel.keyDocsets)
{
[self reload:nil];
}
self.isSearching = YES;
BOOL remotesWereShown = [DHRemoteServer sharedServer].remotes.count > 0;
if(remotesWereShown)
{
[self updateSections:YES];
[self.tableView beginUpdates];
if(remotesWereShown)
{
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
[self.tableView endUpdates];
[self.tableView reloadEmptyDataSet];
self.tableView.tableFooterView = (self.sections.count) ? nil : [UIView new];
}
}
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
self.isSearching = NO;
if(self.needsToReloadWhenDoneSearching || self.viewModel.keyDocsets)
{
self.viewModel.keyDocsets = nil;
self.needsToReloadWhenDoneSearching = NO;
[self reload:nil];
}
else
{
BOOL remotesShouldBeShown = [DHRemoteServer sharedServer].remotes.count > 0;
if(remotesShouldBeShown)
{
[self updateSections:YES];
[self.tableView beginUpdates];
if(remotesShouldBeShown)
{
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
[self.tableView endUpdates];
[self.tableView reloadEmptyDataSet];
self.tableView.tableFooterView = (self.sections.count) ? nil : [UIView new];
}
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self updateTitle];
});
}
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller
{
[self updateTitle];
}
- (void)orientationChanged:(id)sender
{
[self.tableView reloadEmptyDataSet];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:@"DHTypeBrowserSegue"])
{
NSIndexPath *indexPath = self.tableView.indexPathForSelectedRow;
DHDocset *selectedDocset = self.sections[indexPath.section][indexPath.row];
id typeBrowser = [segue destinationViewController];
[typeBrowser setDocset:selectedDocset];
}
else if([[segue identifier] isEqualToString:@"DHRemoteBrowserSegue"])
{
if(isRegularHorizontalClass)
{
[(DHWebView*)[DHWebViewController sharedWebViewController].webView resetHistory];
}
NSIndexPath *indexPath = self.tableView.indexPathForSelectedRow;
DHRemote *selectedRemote = self.sections[indexPath.section][indexPath.row];
id remoteBrowser = [segue destinationViewController];
[remoteBrowser setRemote:selectedRemote];
[selectedRemote connect];
}
else
{
[self.searchController prepareForSegue:segue sender:sender];
}
}
- (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController*)fromVC toViewController:(UIViewController*)toVC
{
// You'll probably never need to modify this, ever!
// What you're looking for is inside DHWebViewController
if([fromVC isKindOfClass:[DHPreferences class]] || [toVC isKindOfClass:[DHPreferences class]])
{
if(![fromVC isKindOfClass:[DHRepo class]] && ![toVC isKindOfClass:[DHRepo class]])
{
return [[DHNavigationAnimator alloc] init];
}
}
return nil;
}
- (IBAction)openSettings:(id)sender
{
if(!self.isActive || (isRegularHorizontalClass && [[self.splitViewController.viewControllers.lastObject topViewController] isKindOfClass:[DHDocsetDownloader class]]))
{
return;
}
[self performSegueWithIdentifier:@"DHOpenSettingsSegue" sender:self];
if(isRegularHorizontalClass)
{
[self performSegueWithIdentifier:@"DHDocsetDownloaderToDetailSegue" sender:self];
[[self.splitViewController.viewControllers.lastObject navigationItem] setHidesBackButton:YES];
}
[self.navigationItem.leftBarButtonItem setEnabled:NO];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if(self.navigationController.viewControllers.count > 1)
{
if([self.navigationController.visibleViewController respondsToSelector:@selector(searchController)] && [[(DHDocsetBrowser*)self.navigationController.visibleViewController searchController] isKindOfClass:[DHDBSearchController class]] && [(DHDocsetBrowser*)self.navigationController.visibleViewController searchController].displayController.active)
{
return NO;
}
else if([self.navigationController.visibleViewController isKindOfClass:[DHPreferences class]])
{
return NO;
}
return YES;
}
return NO;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return [gestureRecognizer isKindOfClass:UIScreenEdgePanGestureRecognizer.class];
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[self.searchController encodeRestorableStateWithCoder:coder];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder];
[self.searchController decodeRestorableStateWithCoder:coder];
}
- (void)applicationFinishedRestoringState
{
[super applicationFinishedRestoringState];
}
+ (NSAttributedString *)titleBarItemAttributedStringTemplate
{
return _titleBarItemAttributedStringTemplate;
}
- (void)grabTitleBarItemAttributedStringTemplate
{
if(_titleBarItemAttributedStringTemplate)
{
return;
}
@try {
for(UIView *view in self.navigationController.navigationBar.subviews)
{
for(UILabel *label in view.subviews)
{
if([label isKindOfClass:[UILabel class]])
{
if([label.text isEqualToString:self.navigationItem.title])
{
_titleBarItemAttributedStringTemplate = [label.attributedText copy];
break;
}
}
}
if(_titleBarItemAttributedStringTemplate)
{
break;
}
}
}
@catch(NSException *exception) { NSLog(@"%@ %@", exception, [exception callStackSymbols]); }
}
- (void)dealloc
{
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
self.navigationController.delegate = nil;
}
@end
================================================
FILE: Dash/DHDocsetBrowserViewModel.h
================================================
#import
@interface DHDocsetBrowserViewModel : NSObject
@property (nonatomic, strong, readonly) NSArray *sections;
@property (nonatomic, strong) NSMutableArray *keyDocsets;
@property (nonatomic, strong) NSArray *shownDocsets;
@property (nonatomic, readonly) BOOL canMoveRows;
- (void)updateSectionsForEditing:(BOOL)editing andSearching:(BOOL)searching;
- (NSArray *)docsetsForEditing:(BOOL)editing;
@end
================================================
FILE: Dash/DHDocsetBrowserViewModel.m
================================================
#import "DHDocsetBrowserViewModel.h"
#import "DHDocsetManager.h"
#import "DHDocsetDownloader.h"
@interface DHDocsetBrowserViewModel ()
@property (nonatomic, strong) NSArray *sections;
@end
@implementation DHDocsetBrowserViewModel
- (BOOL)alphabetizing {
return [NSUserDefaults.standardUserDefaults boolForKey:DHDocsetDownloader.defaultsAlphabetizingKey];
}
- (BOOL)canMoveRows {
return !self.alphabetizing;
}
- (NSArray *)sort:(NSArray *)array {
if (self.alphabetizing) {
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
array = [array sortedArrayUsingDescriptors:@[sortDescriptor]];
}
return array;
}
- (NSArray *)docsetsForEditing:(BOOL)editing {
NSArray *docsets = (editing) ? [DHDocsetManager sharedManager].docsets : (self.keyDocsets) ? self.keyDocsets : [DHDocsetManager sharedManager].enabledDocsets;
return [self sort:docsets];
}
- (void)updateSectionsForEditing:(BOOL)editing andSearching:(BOOL)searching
{
NSMutableArray *sections = [NSMutableArray array];
if([DHRemoteServer sharedServer].remotes.count)
{
if(!editing && !searching)
{
[sections addObject:[self sort:DHRemoteServer.sharedServer.remotes]];
}
}
NSArray *docsets = [self docsetsForEditing:editing];
self.shownDocsets = docsets;
if(docsets.count)
{
[sections addObject:docsets];
}
self.sections = sections;
}
@end
================================================
FILE: Dash/DHDocsetDownloader.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRepo.h"
@interface DHDocsetDownloader : DHRepo
+ (instancetype)sharedDownloader;
- (BOOL)canInstallFeed:(DHFeed *)aFeed;
@end
================================================
FILE: Dash/DHDocsetDownloader.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetDownloader.h"
#import "DHAppDelegate.h"
#import "DHFeed.h"
#import "DDXML.h"
#import "DHFeedResult.h"
#import "DHDocsetManager.h"
@implementation DHDocsetDownloader
static id singleton = nil;
- (void)setUp // doesn't get called unless you call the singleton from DHAppDelegate
{
[super setUp];
NSMutableArray *dashFeeds = [NSMutableArray arrayWithObjects:[DHFeed feedWithFeed:@"NET_Framework.xml" icon:@"net" aliases:@[@"Microsoft .NET Framework", @"C#", @"f#", @"vb", @"visual basic", @"visualbasic", @"vstudio", @"visual studio", @"msdn", @"Microsoft Developer Network"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"ActionScript.xml" icon:@"actionscript" aliases:@[@"adobe flash as3"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Akka.xml" icon:@"akka" aliases:@[@"scala"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Android.xml" icon:@"android" aliases:@[@"java"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Angular.xml" icon:@"angular" aliases:@[@"google angularjs", @"google angular.js", @"angular2", @"angular 2", @"angularjs 2", @"angular.js 2", @"google angularts", @"angular.io angular for typescript angular for ts angular.typescript angular.ts angulartypescript", @"google angular.ts", @"angular2", @"angular 2", @"angularts 2", @"angular.ts 2"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"AngularJS.xml" icon:@"angularjs" aliases:@[@"google angularjs", @"google angular.js", @"angular", @"angularjs", @"angular.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Ansible.xml" icon:@"ansible" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Apache_HTTP_Server.xml" icon:@"apache" aliases:@[@"httpd"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Appcelerator_Titanium.xml" icon:@"titanium" aliases:@[@"Appcelerator Platform"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Apple_API_Reference.xml" icon:@"apple" aliases:@[@"leopard", @"snow leopard", @"lion", @"mountain lion", @"mavericks", @"yosemite", @"macos sierra", @"10.10", @"10.8", @"10.6", @"mac osx", @"10.7", @"10.9", @"10.5", @"xcode", @"apple", @"cocoa", @"objective-c", @"objc", @"macosx", @"macos x", @"swift", @"iphone", @"ipad", @"cocoa touch", @"tvos", @"tvservices", @"apple tv", @"ios", @"iphoneos", @"watchkit"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"AppleScript.xml" icon:@"applescript" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Arduino.xml" icon:@"arduino" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"AWS_JavaScript.xml" icon:@"awsjs" aliases:@[@"aws nodejs", @"aws node.js", @"amazon"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"BackboneJS.xml" icon:@"backbone" aliases:@[@"backbone.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Bash.xml" icon:@"bash" aliases:@[@"bash shell", @"terminal"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Boost.xml" icon:@"boost" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Bootstrap_2.xml" icon:@"bootstrap" aliases:@[@"twitter bootstrap"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Bootstrap_3.xml" icon:@"bootstrap" aliases:@[@"twitter bootstrap"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Bootstrap_4.xml" icon:@"bootstrap" aliases:@[@"twitter bootstrap"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Bourbon.xml" icon:@"bourbon" aliases:@[@"ruby gems", @"rubygems"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"C.xml" icon:@"c" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"C++.xml" icon:@"cpp" aliases:@[@"cpp"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"CakePHP.xml" icon:@"cakephp" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Cappuccino.xml" icon:@"cappuccino" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Chai.xml" icon:@"chai" aliases:@[@"chaijs assertion library", @"chai.js assertion library", @"chai assertion library", @"nodejs", @"node.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Chef.xml" icon:@"chef" aliases:@[@"opscode chef"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Clojure.xml" icon:@"clojure" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"CMake.xml" icon:@"cmake" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Cocos2D.xml" icon:@"cocos2d" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Cocos2D-X.xml" icon:@"cocos2dx" aliases:@[@"cocos2dx", @"cocos2d_x"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Cocos3D.xml" icon:@"cocos2d" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"CodeIgniter.xml" icon:@"codeigniter" aliases:@[@"ellislab codeigniter", @"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"CoffeeScript.xml" icon:@"coffee" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"ColdFusion.xml" icon:@"cf" aliases:@[@"adobe coldfusion", @"cfml"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Common_Lisp.xml" icon:@"lisp" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Compass.xml" icon:@"compass" aliases:@[@"ruby gems", @"rubygems"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Cordova.xml" icon:@"cordova" aliases:@[@"Apache Cordova", @"adobe PhoneGap"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Corona.xml" icon:@"corona" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"CouchDB.xml" icon:@"couchdb" aliases:@[@"Apache CouchDB"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Craft.xml" icon:@"craft" aliases:@[@"craft cms", @"craftcms.com", @"buildwithcraft.com", @"build with craft cms", @"php", @"yii2"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"CSS.xml" icon:@"css" aliases:@[@"mdn", @"mozilla developer network"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"D3JS.xml" icon:@"d3" aliases:@[@"d3.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Dart.xml" icon:@"dartlang" aliases:@[@"dart.js", @"dartjs", @"dartlang", @"dart.js lang", @"dart lang", @"dartjs lang"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Django.xml" icon:@"django" aliases:@[@"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Doctrine_ORM.xml" icon:@"doctrine" aliases:@[@"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Docker.xml" icon:@"docker" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Dojo.xml" icon:@"dojo" aliases:@"dojo toolkit" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"DOM.xml" icon:@"dom" aliases:@[@"dom events", @"mdn", @"mozilla developer network"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Drupal_7.xml" icon:@"drupal" aliases:@[@"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Drupal_8.xml" icon:@"drupal" aliases:@[@"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"ElasticSearch.xml" icon:@"elasticsearch" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Elixir.xml" icon:@"elixir" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Emacs_Lisp.xml" icon:@"elisp" aliases:@[@"elisp"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"EmberJS.xml" icon:@"ember" aliases:@[@"ember-data", @"ember data"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Emmet.xml" icon:@"emmet" aliases:@[@"emmet.io", @"emmetio"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Erlang.xml" icon:@"erlang" aliases:@[@"Erlang OTP"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Express.xml" icon:@"express" aliases:@[@"expressjs", @"nodejs", @"node.js", @"express.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"ExpressionEngine.xml" icon:@"ee" aliases:@[@"ellislab expressionengine ee", @"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"ExtJS.xml" icon:@"extjs" aliases:@[@"sencha extjs", @"sencha ext.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Flask.xml" icon:@"flask" aliases:@[@"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Font_Awesome.xml" icon:@"awesome" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Foundation.xml" icon:@"foundation" aliases:@"zurb foundation" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"GLib.xml" icon:@"glib" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Go.xml" icon:@"go" aliases:@[@"google golang"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Gradle_DSL.xml" icon:@"gradle" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Gradle_Java_API.xml" icon:@"gradle" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Gradle_User_Guide.xml" icon:@"gradle" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Grails.xml" icon:@"grails" aliases:@[@"java"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Groovy.xml" icon:@"groovy" aliases:@[@"java"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Groovy_JDK.xml" icon:@"groovy" aliases:@[@"java"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Grunt.xml" icon:@"grunt" aliases:@[@"nodejs", @"node.js", @"grunt.js", @"gruntjs"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Gulp.xml" icon:@"gulp" aliases:@[@"nodejs", @"node.js", @"gulp.js", @"gulpjs"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Haml.xml" icon:@"haml" aliases:@[@"ruby gems", @"rubygems"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Handlebars.xml" icon:@"handlebars" aliases:@[@"nodejs", @"node.js", @"handlebars.js", @"handlebarsjs"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Haskell.xml" icon:@"haskell" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"HTML.xml" icon:@"html" aliases:@[@"mdn", @"mozilla developer network"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Ionic.xml" icon:@"ionic" aliases:@[@"ionic framework"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"iOS.xml" icon:@"iphone" aliases:@[@"iphone", @"ipad", @"xcode", @"apple", @"cocoa touch", @"objective-c", @"objc", @"swift"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Jasmine.xml" icon:@"jasmine" aliases:@[@"jasminejs", @"jasmine.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Java_EE6.xml" icon:@"jee6" aliases:@[@"javaee6", @"jee6"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_EE7.xml" icon:@"jee7" aliases:@[@"javaee7", @"jee7"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_EE8.xml" icon:@"jee8" aliases:@[@"javaee8", @"jee8"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_SE6.xml" icon:@"java" aliases:@[@"javase6"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_SE7.xml" icon:@"java" aliases:@[@"javase7"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_SE8.xml" icon:@"java" aliases:@[@"javase8"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_SE9.xml" icon:@"java" aliases:@[@"javase9", @"javafx"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_SE10.xml" icon:@"java" aliases:@[@"javase10", @"javafx"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Java_SE11.xml" icon:@"java" aliases:@[@"javase11", @"javafx"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"JavaScript.xml" icon:@"javascript" aliases:@[@"mdn", @"mozilla developer network", @"dom events", @"canvas", @"js"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Jekyll.xml" icon:@"jekyll" aliases:@[@"jekyllrb jekyll.rb jekyll ruby"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Jinja.xml" icon:@"jinja" aliases:@[@"python jinja2 template engine jinja 2 template engine"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Joomla.xml" icon:@"joomla" aliases:@[@"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"jQuery.xml" icon:@"jQuery" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"jQuery_Mobile.xml" icon:@"jquerym" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"jQuery_UI.xml" icon:@"jqueryui" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Julia.xml" icon:@"julia" aliases:@[@"julialang"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"KnockoutJS.xml" icon:@"knockout" aliases:@[@"knockout.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Kobold2D.xml" icon:@"kobold2d" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"LaTeX.xml" icon:@"latex" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Laravel.xml" icon:@"laravel" aliases:@[@"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Less.xml" icon:@"less" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Lo-Dash.xml" icon:@"lodash" aliases:@[@"lodash"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Lua_5.1.xml" icon:@"lua" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Lua_5.2.xml" icon:@"lua" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Lua_5.3.xml" icon:@"lua" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"MarionetteJS.xml" icon:@"marionette" aliases:@[@"backbone marionette.js", @"backbone.marionette.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Markdown.xml" icon:@"markdown" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"MatPlotLib.xml" icon:@"matplotlib" aliases:@[@"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Meteor.xml" icon:@"meteor" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Mocha.xml" icon:@"mocha" aliases:@[@"mochajs mocha.js", @"nodejs", @"node.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"MomentJS.xml" icon:@"moment" aliases:@[@"moment.js", @"nodejs", @"node.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"MongoDB.xml" icon:@"mongodb" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Mongoose.xml" icon:@"mongoose" aliases:@[@"nodejs", @"node.js", @"mongoose.js", @"mongoosejs"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Mono.xml" icon:@"mono" aliases:@[@"xamarin", @"mono touch", @"monotouch", @"monoios", @"mono for ios", @"mono android", @"monoandroid", @"mono for android", @"mono mac", @"monomac", @"mono for mac", @"monoosx", @"mono osx", @"mono for osx", @"xamarin.ios", @"xamarin.mac", @"xamarin.android", @"xamarin.osx", @"xamarin ios", @"xamarin mac", @"xamarin android", @"xamarin osx", @"xamarin for ios", @"xamarin for mac", @"xamarin for android", @"xamarin for osx"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"MooTools.xml" icon:@"moo" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"MySQL.xml" icon:@"mysql" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Neat.xml" icon:@"neat" aliases:@[@"bourbon neat", @"ruby gems", @"rubygems"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Nginx.xml" icon:@"nginx" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"NodeJS.xml" icon:@"nodejs" aliases:@[@"node.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"NumPy.xml" icon:@"numpy" aliases:@[@"scipy", @"sci.py", @"num.py", @"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"OCaml.xml" icon:@"ocaml" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"OpenCV.xml" icon:@"opencv" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"OpenGL_2.xml" icon:@"gl2" aliases:@[@"opengl2"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"OpenGL_3.xml" icon:@"gl3" aliases:@[@"opengl3"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"OpenGL_4.xml" icon:@"gl4" aliases:@[@"opengl4 glsl openglsl"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"macOS.xml" icon:@"Mac" aliases:@[@"leopard", @"snow leopard", @"lion", @"mountain lion", @"mavericks", @"yosemite", @"10.10", @"10.8", @"10.6", @"mac osx", @"mac os x", @"10.7", @"10.9", @"10.5", @"xcode", @"apple", @"cocoa", @"macosx", @"macos x", @"objective-c", @"objc", @"swift"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Pandas.xml" icon:@"pandas" aliases:@[@"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Perl.xml" icon:@"perl" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Phalcon.xml" icon:@"phalcon" aliases:@[@"cphalcon", @"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"PhoneGap.xml" icon:@"phonegap" aliases:@"Apache Cordova adobe phonegap" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"PHP.xml" icon:@"php" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"PHPUnit.xml" icon:@"phpunit" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Play_Java.xml" icon:@"playjava" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Play_Scala.xml" icon:@"playscala" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Polymer.dart.xml" icon:@"polymerdart" aliases:@[@"polymer.dart.js", @"polymer.dartjs", @"polymer.dartlang", @"polymer.dart.js lang", @"polymer.dart lang", @"polymer.dartjs lang"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"PostgreSQL.xml" icon:@"psql" aliases:@[@"psql"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Processing.xml" icon:@"processing" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"PrototypeJS.xml" icon:@"prototype" aliases:@[@"prototype.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Pug.xml" icon:@"pug" aliases:@[@"jade", @"node.js", @"nodejs"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Puppet.xml" icon:@"puppet" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Python_2.xml" icon:@"python" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Python_3.xml" icon:@"python" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Qt_4.xml" icon:@"qt" aliases:@[@"qt4"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Qt_5.xml" icon:@"qt" aliases:@[@"qt5"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"R.xml" icon:@"r" aliases:@[@"r language", @"r project", @"gnu s", @"rlanguage"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Racket.xml" icon:@"racket" aliases:@[@"racketlang racket-lang"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"React.xml" icon:@"react" aliases:@[@"facebook react.js reactjs"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Redis.xml" icon:@"redis" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"RequireJS.xml" icon:@"require" aliases:@[@"require.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Ruby.xml" icon:@"ruby" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Ruby_2.xml" icon:@"ruby" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Ruby_on_Rails_3.xml" icon:@"rails" aliases:@"ror" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Ruby_on_Rails_4.xml" icon:@"rails" aliases:@"ror" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Ruby_on_Rails_5.xml" icon:@"rails" aliases:@"ror" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"RubyMotion.xml" icon:@"rubymotion" aliases:nil doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Rust.xml" icon:@"rust" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"SailsJS.xml" icon:@"sails" aliases:@[@"nodejs sails.js node.js sails.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"SaltStack.xml" icon:@"salt" aliases:@[@"python", @"salt stack"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Sass.xml" icon:@"sass" aliases:@[@"ruby gems", @"rubygems"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Scala.xml" icon:@"scala" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"SciPy.xml" icon:@"scipy" aliases:@[@"numpy", @"sci.py", @"num.py", @"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Semantic_UI.xml" icon:@"semantic" aliases:@[@"Semantic UI"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Sencha_Touch.xml" icon:@"sencha" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Sinon.xml" icon:@"sinon" aliases:@[@"sinonjs nodejs", @"sinon.js node.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Smarty.xml" icon:@"smarty" aliases:@[@"Smarty Template Engine", @"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Sparrow.xml" icon:@"sparrow" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Spring_Framework.xml" icon:@"spring" aliases:@[@"spring.io", @"java"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"SproutCore.xml" icon:@"SproutCore" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"SQLAlchemy.xml" icon:@"sqlalchemy" aliases:@[@"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"SQLite.xml" icon:@"sqlite" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Statamic.xml" icon:@"statamic" aliases:@[@"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Stylus.xml" icon:@"stylus" aliases:@[@"nodejs", @"node.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Susy.xml" icon:@"susy" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"SVG.xml" icon:@"svg" aliases:@[@"mdn", @"mozilla developer network"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Swift.xml" icon:@"swift" aliases:@[@"leopard", @"snow leopard", @"lion", @"mountain lion", @"mavericks", @"yosemite", @"10.10", @"10.8", @"10.6", @"mac osx", @"10.7", @"10.9", @"10.5", @"xcode", @"apple", @"cocoa", @"objective-c", @"objc", @"iphone", @"ipad", @"cocoa touch"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Symfony.xml" icon:@"symfony" aliases:@[@"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Tcl.xml" icon:@"tcl" aliases:@[@"tcl/tk", @"tcltk", @"tcl tk"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Tornado.xml" icon:@"tornado" aliases:@[@"python", @"tornado web server"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"tvOS.xml" icon:@"tvos" aliases:@[@"tvos", @"xcode", @"apple", @"cocoa", @"objective-c", @"objc", @"swift", @"tvservices", @"apple tv"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Twig.xml" icon:@"twig" aliases:@[@"Twig Template Engine", @"php"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Twisted.xml" icon:@"twisted" aliases:@[@"twisted matrix", @"python"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"TypeScript.xml" icon:@"typescript" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"TYPO3.xml" icon:@"typo3" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"UnderscoreJS.xml" icon:@"underscore" aliases:@[@"underscore.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Unity_3D.xml" icon:@"unity3d" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Vagrant.xml" icon:@"vagrant" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Vim.xml" icon:@"vim" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"VMware_vSphere.xml" icon:@"vsphere" aliases:nil doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"VueJS.xml" icon:@"vue" aliases:@[@"nodejs vue.js node.js vue.js"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"watchOS.xml" icon:@"watchos" aliases:@[@"iphoneos", @"ios", @"ipad", @"xcode", @"apple", @"cocoa touch", @"objective-c", @"objc", @"swift", @"watchkit"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"WordPress.xml" icon:@"wordpress" aliases:@[@"php"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Xamarin.xml" icon:@"xamarin" aliases:@[@"xamarin.ios", @"xamarin.mac", @"xamarin.android", @"xamarin.osx", @"xamarin ios", @"xamarin mac", @"xamarin android", @"xamarin osx", @"xamarin for ios", @"xamarin for mac", @"xamarin for android", @"xamarin for osx"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Xojo.xml" icon:@"xojo" aliases:@[@"realbasic", @"real basic", @"real studio", @"realstudio"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"XSLT.xml" icon:@"xslt" aliases:@[@"mdn", @"mozilla developer network", @"exslt", @"xpath"] doesNotHaveVersions:YES],
[DHFeed feedWithFeed:@"Yii.xml" icon:@"yii" aliases:@"yii framework yii2 framework php" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"YUI.xml" icon:@"yui" aliases:@"yui library" doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Zend_Framework_1.xml" icon:@"zend" aliases:@[@"zf1"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Zend_Framework_2.xml" icon:@"zend" aliases:@[@"zf2"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"Zend_Framework_3.xml" icon:@"zend" aliases:@[@"zf3"] doesNotHaveVersions:NO],
[DHFeed feedWithFeed:@"ZeptoJS.xml" icon:@"zepto" aliases:@[@"Zepto.js"] doesNotHaveVersions:NO],
nil];
NSArray *savedFeeds = [[NSUserDefaults standardUserDefaults] objectForKey:[self defaultsKey]];
for(NSDictionary *feedDictionary in savedFeeds)
{
DHFeed *savedFeed = [DHFeed feedWithDictionaryRepresentation:feedDictionary];
if([@[@"http://kapeli.com/feeds/OS_X.xml", @"http://kapeli.com/feeds/watchOS.xml", @"http://kapeli.com/feeds/iOS.xml", @"http://kapeli.com/feeds/tvOS.xml", @"http://kapeli.com/feeds/Jade.xml", @"http://kapeli.com/feeds/JavaFX.xml", @"http://kapeli.com/feeds/Angular.dart.xml", @"http://kapeli.com/feeds/AngularTS.xml", @"http://kapeli.com/feeds/Gradle_Groovy_API.xml", @"http://kapeli.com/feeds/XUL.xml", @"http://kapeli.com/feeds/OpenCV_C.xml", @"http://kapeli.com/feeds/OpenCV_C++.xml", @"http://kapeli.com/feeds/OpenCV_Java.xml", @"http://kapeli.com/feeds/OpenCV_Python.xml"] containsObject:savedFeed.feedURL])
{
if(savedFeed.installed)
{
NSString *trashPath = [self uniqueTrashPath];
NSString *feedPath = [self docsetPathForFeed:savedFeed];
NSFileManager *fileManager = [NSFileManager defaultManager];
[[DHDocsetManager sharedManager] removeDocsetsInFolder:feedPath];
[fileManager moveItemAtPath:feedPath toPath:trashPath error:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[self emptyTrashAtPath:trashPath];
});
}
savedFeed.installed = NO;
savedFeed.installedVersion = nil;
}
if([@[@"http://kapeli.com/feeds/OS_X.xml", @"http://kapeli.com/feeds/Jade.xml", @"http://kapeli.com/feeds/JavaFX.xml", @"http://kapeli.com/feeds/Apple_Guides_and_Sample_Code.xml", @"http://kapeli.com/feeds/Angular.dart.xml", @"http://kapeli.com/feeds/AngularTS.xml", @"http://kapeli.com/feeds/Gradle_Groovy_API.xml", @"http://kapeli.com/feeds/XUL.xml", @"http://kapeli.com/feeds/OpenCV_C.xml", @"http://kapeli.com/feeds/OpenCV_C++.xml", @"http://kapeli.com/feeds/OpenCV_Java.xml", @"http://kapeli.com/feeds/OpenCV_Python.xml"] containsObject:savedFeed.feedURL])
{
continue;
}
NSUInteger index = [dashFeeds indexOfObjectPassingTest:^BOOL(DHFeed *obj, NSUInteger idx, BOOL *stop) {
return [[obj feed] isEqualToString:savedFeed.feed];
}];
if(index != NSNotFound)
{
DHFeed *dashFeed = dashFeeds[index];
dashFeed.installed = savedFeed.installed;
dashFeed.installedVersion = savedFeed.installedVersion;
dashFeed.size = savedFeed.size;
}
else
{
[dashFeeds addObject:savedFeed];
}
}
[dashFeeds sortUsingFunction:compareFeeds context:nil];
self.feeds = dashFeeds;
}
- (NSString *)installFeed:(DHFeed *)feed isAnUpdate:(BOOL)isAnUpdate
{
NSObject *identifier = [[NSObject alloc] init];
feed.identifier = identifier;
feed.waiting = YES;
BOOL didStallOnce = NO;
BOOL didSetStallLabel = NO;
while([self shouldStall] && feed.installing && feed.identifier == identifier)
{
if(didStallOnce && !didSetStallLabel)
{
didSetStallLabel = YES;
dispatch_sync(dispatch_get_main_queue(), ^{
[feed setDetailString:@"Waiting..."];
[[feed cell].titleLabel setRightDetailText:@"Waiting..." adjustMainWidth:YES];
[feed setMaxRightDetailWidth:[feed cell].titleLabel.maxRightDetailWidth];
});
}
didStallOnce = YES;
[NSThread sleepForTimeInterval:1.0f];
}
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
feed.waiting = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *installPath = [self docsetPathForFeed:feed];
NSString *tempPath = [self uniqueTempDirAtPath:installPath];
NSString *tempFile = [tempPath stringByAppendingPathComponent:@"dash_temp_docset.tgz"];
NSString *tarixFile = [tempFile stringByAppendingString:@".tarix"];
__block BOOL shouldWait = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
shouldWait = [[DHLatencyTester sharedLatency] performTests:NO];
});
if(shouldWait)
{
[NSThread sleepForTimeInterval:3.0f];
}
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
NSString *error = nil;
DHFeedResult *feedResult = [self loadFeed:feed error:&error];
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
if(feedResult && feed.installing)
{
feed.feedResult = feedResult;
feedResult.feed = feed;
NSError *downloadError = nil;
if(feedResult.downloadURLs.count)
{
NSString *downloadURL = feedResult.downloadURLs[0];
[self emptyTrashAtPath:tempPath];
if(![fileManager createDirectoryAtPath:tempPath withIntermediateDirectories:YES attributes:nil error:nil])
{
return @"Couldn't create install directory.";
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
NSURL *url = [NSURL URLWithString:downloadURL];
if(url)
{
BOOL result = NO;
NSURL *tarixURL = [NSURL URLWithString:[[downloadURL stringByAppendingString:@".tarix"] stringByConvertingKapeliHttpURLToHttps]];
feedResult.hasTarix = [NSURL URLIsFound:[tarixURL absoluteString] timeoutInterval:120.0f checkForRedirect:YES];
downloadError = nil;
#ifdef DEBUG
NSLog(@"Downloading %@", url);
#endif
if([DHFileDownload downloadItemAtURL:url toFile:tempFile error:&downloadError delegate:self identifier:feedResult] && !downloadError)
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Waiting..."];
@synchronized([DHDocsetIndexer class])
{
if(!feedResult.hasTarix)
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[fileManager removeItemAtPath:tarixFile error:nil];
result = [DHUnarchiver unarchiveArchive:tempFile delegate:feedResult];
}
else
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Preparing..."];
result = [DHFileDownload downloadItemAtURL:tarixURL toFile:tarixFile error:nil delegate:nil identifier:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't download index file.";
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Extracting..."];
result = [DHUnarchiver unarchiveArchive:tarixFile delegate:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive index file.";
}
[fileManager removeItemAtPath:tarixFile error:nil];
tarixFile = [fileManager firstFileWithExtension:@"tarix" atPath:tempPath ignoreHidden:YES];
if(!tarixFile)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't find index file.";
}
tarixFile = [tempPath stringByAppendingPathComponent:tarixFile];
result = [DHUnarchiver unpackTarixDocset:tempFile tarixPath:tarixFile delegate:feedResult];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive docset.";
}
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive docset.";
}
if(!feedResult.hasTarix)
{
[fileManager removeItemAtPath:tempFile error:nil];
}
DHDocset *docset = [DHDocset firstDocsetInsideFolder:tempPath];
if(!docset)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't install docset.";
}
else if(feedResult.hasTarix)
{
[fileManager moveItemAtPath:tarixFile toPath:docset.tarixIndexPath error:nil];
[fileManager moveItemAtPath:tempFile toPath:docset.tarixPath error:nil];
}
[DHDocsetIndexer indexerForDocset:docset delegate:feedResult];
[fileManager removeItemAtPath:docset.sqlPath error:nil];
[fileManager removeItemAtPath:[docset.sqlPath stringByAppendingString:@"-shm"] error:nil];
[fileManager removeItemAtPath:[docset.sqlPath stringByAppendingString:@"-wal"] error:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:installPath];
NSString *file = nil;
while(file = [dirEnum nextObject])
{
[dirEnum skipDescendents];
NSString *filePath = [installPath stringByAppendingPathComponent:file];
if(![filePath isEqualToString:tempPath])
{
NSString *trashPath = [self uniqueTrashPath];
[fileManager moveItemAtPath:filePath toPath:trashPath error:nil];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[self emptyTrashAtPath:trashPath];
});
}
}
[fileManager moveItemAtPath:docset.path toPath:[installPath stringByAppendingPathComponent:[docset.path lastPathComponent]] error:nil];
[self emptyTrashAtPath:tempPath];
return nil;
}
}
else if(downloadError.code == DHDownloadCancelled)
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
else
{
[self emptyTrashAtPath:tempPath];
}
}
}
return @"Couldn't download docset.";
}
else
{
return error;
}
return nil;
}
// This does not modify the feed at all
- (DHFeedResult *)loadFeed:(DHFeed *)feed error:(NSString **)returnError
{
@try
{
__block NSError *error = nil;
NSString *originalFeedURL = feed.feedURL;
NSString *lastPathComponent = [originalFeedURL lastPathComponent];
BOOL isDash = !feed.isCustom;
if(originalFeedURL)
{
NSMutableArray *feedURLs = [NSMutableArray arrayWithObject:originalFeedURL];
if(isDash)
{
NSString *cdnFeedURL = [originalFeedURL substringFromStringReturningNil:@"/feeds/"];
cdnFeedURL = (cdnFeedURL) ? [[[DHLatencyTester sharedLatency] bestMirror] stringByAppendingString:cdnFeedURL] : nil;
cdnFeedURL = [cdnFeedURL stringByConvertingKapeliHttpURLToHttps];
if(cdnFeedURL)
{
feedURLs = [NSMutableArray arrayWithObject:cdnFeedURL];
NSString *secondBest = [[[DHLatencyTester sharedLatency] secondBestMirror] stringByAppendingString:[originalFeedURL substringFromStringReturningNil:@"/feeds/"]];
secondBest = [secondBest stringByConvertingKapeliHttpURLToHttps];
[feedURLs addObject:secondBest];
}
else
{
[feedURLs addObject:[@"https://kapeli.com/feeds/" stringByAppendingString:lastPathComponent]];
}
}
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *xmls = [NSMutableArray array];
NSLock *workerLock = [[NSLock alloc] init];
__block BOOL succeeded = NO;
__block NSInteger workerCount = feedURLs.count;
for(NSString *feedURL in feedURLs)
{
dispatch_queue_t myQueue = dispatch_queue_create([feedURL UTF8String], 0);
dispatch_async(myQueue, ^{
NSURL *xmlURL = [NSURL URLWithString:feedURL];
BOOL didSucceed = NO;
NSError *workingError = nil;
if(xmlURL)
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:xmlURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:90.0f];
[request setValue:[[NSBundle mainBundle] bundleIdentifier] forHTTPHeaderField:@"User-Agent"];
NSURLResponse *response = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&workingError];
if(data && !workingError)
{
NSString *xmlString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if(xmlString && xmlString.length)
{
NSXMLDocument *xml = [[NSXMLDocument alloc] initWithXMLString:xmlString options:0 error:&workingError];
NSXMLElement *root = [xml rootElement];
if([[root name] isCaseInsensitiveEqual:@"feed"])
{
root = [[root elementsForName:@"entry"] count] > 0 ? [root elementsForName:@"entry"][0] : nil;
}
if(xml && root && ([root elementsForName:@"version"].count || [root elementsForName:@"docset:version"].count) && ([root elementsForName:@"url"].count || [root elementsForName:@"link"].count))
{
[xmls addObject:xml];
[condition lock];
[condition signal];
[condition unlock];
didSucceed = YES;
}
}
}
}
[workerLock lock];
if(didSucceed)
{
succeeded = YES;
}
BOOL otherSucceeded = succeeded;
--workerCount;
BOOL isLast = workerCount == 0;
[workerLock unlock];
if(isLast && !didSucceed && !otherSucceeded)
{
error = workingError;
[condition lock];
[condition signal];
[condition unlock];
}
});
}
[condition lock];
[condition wait];
[condition unlock];
NSXMLDocument *xml = nil;
if(xmls.count)
{
xml = xmls[0];
}
if(!xml)
{
if([error localizedDescription])
{
*returnError = @"Unable to load docset feed.";
}
return nil;
}
else
{
NSXMLElement *root = [xml rootElement];
BOOL isXcode = NO;
if([[root name] isCaseInsensitiveEqual:@"feed"])
{
root = [[root elementsForName:@"entry"] count] > 0 ? [root elementsForName:@"entry"][0] : nil;
isXcode = YES;
}
if(root)
{
if([[root name] isCaseInsensitiveEqual:@"entry"])
{
NSArray *versions = [root elementsForName:(!isXcode) ? @"version" : @"docset:version"];
NSString *iosVersion = [[[root elementsForName:@"ios_version"] firstObject] stringValue];
if(versions.count == 1)
{
if([versions[0] stringValue].length)
{
DHFeedResult *result = [[DHFeedResult alloc] init];
NSString *version = [versions[0] stringValue];
if(iosVersion && iosVersion.length)
{
version = [version stringByAppendingFormat:@"/%@", iosVersion];
}
result.version = version;
NSArray *docsetURLs = [root elementsForName:(!isXcode) ? @"url" : @"link"];
NSMutableArray *urlResults = [NSMutableArray array];
for(NSXMLElement *docsetURL in docsetURLs)
{
NSString *urlString = (!isXcode) ? [docsetURL stringValue] : [[docsetURL attributeForName:@"href"] stringValue];
if(urlString.length)
{
[urlResults addObject:[urlString trimWhitespace]];
}
}
if(isDash)
{
[[DHLatencyTester sharedLatency] sortURLsBasedOnLatency:urlResults];
}
if(urlResults.count)
{
if(isXcode && [root elementsForName:@"title"].count)
{
NSString *xcodeName = [[root elementsForName:@"title"][0] stringValue];
if(xcodeName && xcodeName.length)
{
// [feed setObject:xcodeName forKey:@"xcodeName"];
}
}
result.downloadURLs = urlResults;
return result;
}
else
{
*returnError = @"Unable to load docset feed.";
}
}
}
}
}
*returnError = @"Unable to parse docset feed.";
return nil;
}
}
}
@catch (NSException *exception)
{
NSLog(@"FIXME: exception in loadFeed: %@", exception);
NSLog(@"%@", [NSThread callStackSymbols]);
}
return nil;
}
- (NSString *)docsetInstallFolderName
{
return @"Dash";
}
- (BOOL)canInstallFeed:(DHFeed *)feed
{
NSString *title = nil;
NSString *message = nil;
if([feed.feedURL isEqualToString:@"http://kapeli.com/feeds/DOM.xml"])
{
title = @"DOM Documentation";
message = @"There is no DOM docset. DOM documentation can be found in the JavaScript docset. Please install the JavaScript docset instead.";
}
else if([feed.feedURL isEqualToString:@"http://kapeli.com/feeds/RubyMotion.xml"])
{
title = @"RubyMotion Documentation";
message = @"RubyMotion had to remove its API documentation due to legal reasons. Please contact the RubyMotion team for more details.\n\nIn the meantime, you can use the Apple API Reference docset instead.";
}
else if([feed.feedURL isEqualToString:@"http://kapeli.com/feeds/Apple_API_Reference.xml"])
{
title = @"Apple API Reference";
message = @"To install the Apple API Reference docset you need to:\n\n1. Use Dash for macOS to install the Apple API Reference docset from Preferences > Downloads\n2. Go to Preferences > Docsets, right click the Apple API Reference docset and select \"Generate iOS Compatible Docset\"\n3. Transfer the resulting docset using iTunes File Sharing or AirDrop";
}
else if([@[@"http://kapeli.com/feeds/OS_X.xml", @"http://kapeli.com/feeds/macOS.xml", @"http://kapeli.com/feeds/watchOS.xml", @"http://kapeli.com/feeds/iOS.xml", @"http://kapeli.com/feeds/tvOS.xml"] containsObject:feed.feedURL])
{
title = @"Apple API Reference";
NSString *name = [[[feed.feedURL lastPathComponent] stringByDeletingPathExtension] stringByReplacingOccurrencesOfString:@"_" withString:@" "];
message = [NSString stringWithFormat:@"There is no %@ docset. The documentation for %@ can be found inside the Apple API Reference docset. \n\nTo install the Apple API Reference docset you need to:\n\n1. Use Dash for macOS to install the docset from Preferences > Downloads\n2. Go to Preferences > Docsets, right click the Apple API Reference docset and select \"Generate iOS-compatible Docset\"\n3. Transfer the resulting docset using iTunes File Sharing or AirDrop", name, name];
}
if(title && message)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
return NO;
}
return YES;
}
+ (instancetype)sharedDownloader
{
if(singleton)
{
return singleton;
}
id downloader = [[DHAppDelegate mainStoryboard] instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
[downloader setUp];
return downloader;
}
+ (id)alloc
{
if(singleton)
{
return singleton;
}
return [super alloc];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if(singleton)
{
return singleton;
}
self = [super initWithCoder:aDecoder];
singleton = self;
return self;
}
@end
================================================
FILE: Dash/DHDocsetIndexer.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHDocsetIndexer : NSObject
@property (weak) id delegate;
@property (strong) DHDocset *docset;
@property (assign) BOOL hasV2Guides;
@property (assign) NSInteger progressCount;
@property (assign) NSInteger currentProgress;
@property (assign) double lastDisplayedPercent;
+ (DHDocsetIndexer *)indexerForDocset:(DHDocset *)docset delegate:(id)delegate;
@end
================================================
FILE: Dash/DHDocsetIndexer.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetIndexer.h"
#import "DHTypes.h"
#import "DHFeedResult.h"
@implementation DHDocsetIndexer
+ (DHDocsetIndexer *)indexerForDocset:(DHDocset *)docset delegate:(id)delegate
{
DHDocsetIndexer *indexer = [[DHDocsetIndexer alloc] init];
indexer.docset = docset;
indexer.delegate = delegate;
if(![delegate isCancelled])
{
[delegate setRightDetail:@"Indexing..."];
[indexer prepare];
[indexer index];
}
return indexer;
}
- (void)prepare
{
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtPath:self.docset.optimisedIndexPath error:nil];
[fileManager removeItemAtPath:[self.docset.optimisedIndexPath stringByAppendingString:@"-journal"] error:nil];
[fileManager removeItemAtPath:self.docset.tempOptimisedIndexPath error:nil];
[fileManager removeItemAtPath:[self.docset.tempOptimisedIndexPath stringByAppendingString:@"-journal"] error:nil];
[self.docset executeBlockWithinDocsetDBConnection:^(FMDatabase *db) {
FMResultSet *rs = [db executeQuery:([self.docset isDashDocset]) ? @"SELECT COUNT(id) from searchIndex" : @"SELECT COUNT(Z_PK) FROM ZTOKEN"];
if([rs next])
{
self.progressCount = [rs intForColumnIndex:0];
}
#ifdef DEBUG
[db setLogsErrors:NO];
#endif
rs = [db executeQuery:@"SELECT COUNT(Z_PK) FROM ZNODE WHERE ZPRIMARYPARENT IS NOT NULL AND ZKDOCUMENTTYPE < 2 AND ZKNODETYPE = 'folder' AND (ZKPATH NOT LIKE 'navigation%' OR ZKPATH IS NULL)"];
if([rs next])
{
self.progressCount += [rs intForColumnIndex:0];
}
else
{
rs = [db executeQuery:@"SELECT COUNT(z.Z_PK) FROM ZNODE z, ZNODEURL u WHERE z.Z_PK = u.ZNODE AND z.ZPRIMARYPARENT IS NOT NULL AND z.ZKDOCUMENTTYPE < 2 AND z.ZKNODETYPE = 2 AND (u.ZPATH NOT LIKE 'navigation%' OR u.ZPATH IS NULL)"];
if([rs next])
{
self.hasV2Guides = YES;
self.progressCount += [rs intForColumnIndex:0];
}
}
} readOnly:YES lockCondition:DHLockDontLock optimisedIndex:NO];
}
- (void)index
{
@autoreleasepool {
@try {
[self.docset executeBlockWithinDocsetDBConnection:^(FMDatabase *indexDB) {
[self.docset executeBlockWithinDocsetDBConnection:^(FMDatabase *db) {
[self checkIfCancelled];
[indexDB beginDeferredTransaction];
[indexDB executeUpdate:@"CREATE TABLE searchIndex(rowid INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT)"];
[indexDB executeUpdate:@"CREATE VIRTUAL TABLE queryIndex USING FTS4 (perfect, prefix, suffixes, matchinfo=fts3, compress=dashCompress, uncompress=dashUncompress, tokenize=simple XX [* ])"];
[indexDB executeUpdate:@"CREATE VIEW IF NOT EXISTS wholeIndex AS SELECT queryIndex.rowid AS rowid, name, type, path, perfect, prefix, suffixes FROM searchIndex JOIN queryIndex ON searchIndex.rowid = queryIndex.rowid;"];
[indexDB executeUpdate:@"CREATE TRIGGER index_insert INSTEAD OF INSERT ON wholeIndex\n"
"BEGIN\n"
"INSERT INTO searchIndex (name, type, path) VALUES (NEW.name, NEW.type, NEW.path);\n"
"INSERT INTO queryIndex (rowid, perfect, prefix, suffixes) VALUES (last_insert_rowid(), NEW.perfect, NEW.prefix, NEW.suffixes);\n"
"END;\n"];
BOOL isDash = self.docset.isDashDocset;
NSString *platform = self.docset.platform;
BOOL isMacOSX = [platform isEqualToString:@"macosx"] || [platform isEqualToString:@"osx"];
BOOL isApple = isMacOSX || [platform isEqualToString:@"ios"] || [platform isEqualToString:@"iphoneos"] || [platform isEqualToString:@"watchos"] || [platform isEqualToString:@"tvos"];
NSString *indexQuery = (isDash) ? @"SELECT path, 1, name, type, rowid FROM searchIndex " : @"SELECT f.ZPATH, m.ZANCHOR, t.ZTOKENNAME, ty.ZTYPENAME, t.rowid FROM ZTOKEN t, ZTOKENTYPE ty, ZFILEPATH f, ZTOKENMETAINFORMATION m WHERE ty.Z_PK = t.ZTOKENTYPE AND f.Z_PK = m.ZFILE AND m.ZTOKEN = t.Z_PK ";
BOOL hasTOKENUSR = NO;
if(!isDash && isApple)
{
hasTOKENUSR = [db columnExists:@"ZTOKENUSR" inTableWithName:@"ZTOKEN"];
if(hasTOKENUSR)
{
indexQuery = @"SELECT CASE WHEN m.ZFILE IS NOT NULL THEN f.ZPATH ELSE u.ZPATH END, CASE WHEN m.ZANCHOR IS NOT NULL THEN m.ZANCHOR ELSE u.ZANCHOR END, t.ZTOKENNAME, ty.ZTYPENAME, t.rowid, t.ZTOKENUSR FROM ZTOKEN t, ZTOKENTYPE ty, ZTOKENMETAINFORMATION m LEFT JOIN ZFILEPATH f ON m.ZFILE = f.Z_PK LEFT JOIN ZNODEURL u ON t.ZPARENTNODE = u.ZNODE WHERE ty.Z_PK = t.ZTOKENTYPE AND m.ZTOKEN = t.Z_PK ";
}
}
indexQuery = [indexQuery stringByAppendingString:[[DHTypes sharedTypes] unifiedSQLiteOrder:isDash platform:platform]];
if(hasTOKENUSR)
{
indexQuery = [indexQuery stringByReplacingOccurrencesOfString:@" ORDER BY " withString:@" ORDER BY f.ZPATH is NULL, "];
}
[self checkIfCancelled];
FMResultSet *rs = [db executeQuery:indexQuery];
BOOL next = [rs next];
BOOL isFetchingGuides = NO;
NSMutableSet *duplicates = [NSMutableSet set];
NSMutableDictionary *tokenUsers = [NSMutableDictionary dictionary];
NSInteger count = 0;
BOOL firstLoop = YES;
while(next || firstLoop)
{
@autoreleasepool {
NSString *anchor = nil;
if(!next && firstLoop)
{
firstLoop = NO;
goto next;
}
firstLoop = NO;
[self checkIfCancelled];
[self incrementProgressBy:1];
anchor = (isDash) ? @"" : [rs stringForColumnIndex:1];
if(isFetchingGuides || !(isMacOSX && anchor && anchor.length && [anchor rangeOfString:@"java" options:NSCaseInsensitiveSearch].location != NSNotFound))
{
NSString *path = [rs stringForColumnIndex:0];
NSString *name = [rs stringForColumnIndex:2];
NSString *type = [rs stringForColumnIndex:3];
type = [DHTypes singularFromEncoded:type notFoundReturn:@"Variable"];
if(hasTOKENUSR && !isFetchingGuides)
{
NSString *tokenUser = [rs stringForColumnIndex:5];
if(tokenUser && tokenUser.length)
{
if(!path)
{
NSArray *data = tokenUsers[tokenUser];
path = data[0];
anchor = data[1];
}
else
{
tokenUsers[tokenUser] = @[path, (anchor) ? [anchor stringByAppendingString:@"-dash-swift-hack"] : @"-dash-swift-hack"];
}
}
}
if((!path || !path.length) && hasTOKENUSR && isApple && ([name hasPrefix:@"WKInterface"] || [name hasPrefix:@"WKUserNotificationInterface"]) && [type isEqualToString:@"cl"])
{
path = [NSString stringWithFormat:@"documentation/WatchKit/Reference/%@_class/index.html", name];
}
if(isFetchingGuides)
{
if(!path || !path.length)
{
path = [@"ghttp://" stringByAppendingString:type];
}
else
{
path = [@"gfile://" stringByAppendingString:path];
}
NSInteger docType = [rs intForColumnIndex:4];
type = (docType == 0) ? @"Guide" : @"Sample";
}
if(!path || !path.length || !name || !name.length || !type || !type.length)
{
goto next;
}
if(anchor && anchor.length)
{
path = [path stringByAppendingFormat:@"#%@", anchor];
}
if(name.length > 200)
{
name = [name substringToIndex:200];
}
NSString *whitespaceFree = [name stringByReplacingOccurrencesOfString:@" " withString:@""];
if(whitespaceFree.length)
{
if(isApple)
{
if([name isEqualToString:@"NSMutableAttributedString"] && [path contains:@"UIKit/Reference/NSMutableAttributedString_UIKit_Additions/"])
{
path = [path stringByReplacingOccurrencesOfString:@"UIKit/Reference/NSMutableAttributedString_UIKit_Additions/" withString:@"Cocoa/Reference/Foundation/Classes/NSMutableAttributedString_Class/"];
}
else if([name isEqualToString:@"NSView"] && [path contains:@"MacOSXServer/"])
{
NSString *hash = [path substringFromString:@"#"];
path = [[[[path substringToString:@"MacOSXServer/"] stringByAppendingString:@"Cocoa/Reference/ApplicationKit/Classes/NSView_Class/index.html"] stringByAppendingString:@"#"] stringByAppendingString:hash];
}
else if([name isEqualToString:@"NSATSTypesetter"] && [path contains:@"Cocoa/Reference/Foundation/Classes/NSXMLDTD_Class/"])
{
path = [path stringByReplacingOccurrencesOfString:@"Cocoa/Reference/Foundation/Classes/NSXMLDTD_Class/" withString:@"Cocoa/Reference/ApplicationKit/Classes/NSATSTypesetter_Class/"];
}
else if([name isEqualToString:@"NSDockTile"] && [path contains:@"Cocoa/Reference/NSTextInputClient_Protocol/"])
{
path = [path stringByReplacingOccurrencesOfString:@"Cocoa/Reference/NSTextInputClient_Protocol/" withString:@"Cocoa/Reference/NSDockTile_Class/"];
}
else if([name isEqualToString:@"NSFetchRequest"] && [path contains:@"Cocoa/Reference/CoreDataFramework/Classes/NSEntityDescription_Class/"])
{
path = [path stringByReplacingOccurrencesOfString:@"Cocoa/Reference/CoreDataFramework/Classes/NSEntityDescription_Class/" withString:@"Cocoa/Reference/CoreDataFramework/Classes/NSFetchRequest_Class/"];
}
else if([name isEqualToString:@"NSManagedObject"] && [path contains:@"Cocoa/Reference/CoreDataFramework/Classes/NSManagedObjectID_Class/"])
{
path = [path stringByReplacingOccurrencesOfString:@"Cocoa/Reference/CoreDataFramework/Classes/NSManagedObjectID_Class/" withString:@"Cocoa/Reference/CoreDataFramework/Classes/NSManagedObject_Class/"];
}
else if([@[@"Menu", @"DRBurnRef", @"DRFileRef", @"DRTrackRef", @"CSIdentity", @"AXObserver", @"DREraseRef", @"DRDeviceRef", @"DRFolderRef", @"CSIdentityQuery", @"DRCDTextBlockRef", @"CSIdentityAuthority", @"DRNotificationCenterRef"] containsObject:name] && ![path contains:@"#"] && [type isCaseInsensitiveEqual:@"cl"])
{
NSString *fixName = name;
if(![name hasSuffix:@"Ref"])
{
fixName = [name stringByAppendingString:@"Ref"];
}
path = [path stringByAppendingFormat:@"#//apple_ref/c/tdef/%@", fixName];
}
}
NSString *duplicateHash = [NSString stringWithFormat:@"%@%@%@", name, type, path];
if(![duplicates containsObject:duplicateHash])
{
[duplicates addObject:duplicateHash];
[indexDB executeUpdate:@"INSERT INTO wholeIndex(name, type, path, perfect, prefix, suffixes) VALUES(?, ?, ?, ?, ?, ?);", name, type, path, [whitespaceFree stringByReplacingSpecialFTSCharacters], [whitespaceFree FTSPrefix], [whitespaceFree allFTSSuffixes]];
++count;
if(count % 2500 == 0)
{
[indexDB commit];
[indexDB beginDeferredTransaction];
}
[self checkIfCancelled];
}
}
}
next:
if(next)
{
next = [rs next];
}
if(!next && !isFetchingGuides)
{
#ifdef DEBUG
[db setLogsErrors:NO];
#endif
rs = [db executeQuery:(!self.hasV2Guides) ? @"SELECT ZKPATH, ZKANCHOR, ZKNAME, ZKURL, ZKDOCUMENTTYPE, rowid FROM ZNODE WHERE ZPRIMARYPARENT IS NOT NULL AND ZKDOCUMENTTYPE < 2 AND ZKNODETYPE = 'folder' AND (ZKPATH NOT LIKE 'navigation%' OR ZKPATH IS NULL)" : @"SELECT u.ZPATH, u.ZANCHOR, z.ZKNAME, u.ZBASEURL, z.ZKDOCUMENTTYPE, z.rowid FROM ZNODE z, ZNODEURL u WHERE z.Z_PK = u.ZNODE AND z.ZPRIMARYPARENT IS NOT NULL AND z.ZKDOCUMENTTYPE < 2 AND z.ZKNODETYPE = 2 AND (u.ZPATH NOT LIKE 'navigation%' OR u.ZPATH IS NULL)"];
next = [rs next];
isFetchingGuides = YES;
}
}
}
[self checkIfCancelled];
[indexDB executeUpdate:[NSString stringWithFormat:@"INSERT INTO queryIndex(queryIndex) VALUES('optimize');"]];
[self checkIfCancelled];
[indexDB commit];
} readOnly:YES lockCondition:DHLockDontLock optimisedIndex:NO];
} readOnly:NO lockCondition:DHLockDontLock optimisedIndex:YES];
}
@catch(NSException *exception) {
if(![[exception name] isEqualToString:@"Indexing Interrupt"])
{
NSLog(@"FIXME: exception in index");
NSLog(@"%@", exception);
NSLog(@"%@", [NSThread callStackSymbols]);
}
}
@finally {
if(![self.delegate isCancelled])
{
if(self.currentProgress < self.progressCount && self.progressCount > 0)
{
[self incrementProgressBy:self.progressCount-self.currentProgress];
}
}
}
}
}
- (void)incrementProgressBy:(NSUInteger)increment;
{
self.currentProgress += increment;
if(self.currentProgress > self.progressCount)
{
self.currentProgress = self.progressCount;
}
double currentPercent = (double)self.currentProgress/self.progressCount;
double delta = currentPercent - self.lastDisplayedPercent;
if(delta > 0.005f || delta < -0.005f || (self.currentProgress == self.progressCount && self.lastDisplayedPercent < 1.0))
{
self.lastDisplayedPercent = currentPercent;
[self.delegate setIndexingProgress:currentPercent];
}
}
- (void)checkIfCancelled
{
if([self.delegate isCancelled])
{
[NSException raise:@"Indexing Interrupt" format:@""];
}
}
@end
================================================
FILE: Dash/DHDocsetManager.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHDocsetManager : NSObject
@property (strong) NSMutableArray *docsets;
+ (DHDocsetManager *)sharedManager;
- (void)addDocset:(DHDocset *)docset andRemoveOthers:(BOOL)shouldRemove removeOnlyEqualPaths:(BOOL)removeOnlyEqualPaths;
- (void)removeDocsetsInFolder:(NSString *)path;
- (void)moveDocsetAtIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex;
- (DHDocset *)docsetForDocumentationPage:(NSString *)url;
- (NSMutableArray *)enabledDocsets;
- (void)saveDefaults;
- (DHDocset *)docsetWithRelativePath:(NSString *)relativePath;
- (DHDocset *)appleAPIReferenceDocset;
@end
#define DHDocsetsChangedNotification @"DHDocsetsChangedNotification"
================================================
FILE: Dash/DHDocsetManager.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetManager.h"
@implementation DHDocsetManager
+ (DHDocsetManager *)sharedManager
{
static dispatch_once_t pred;
static DHDocsetManager *_docsetManager = nil;
dispatch_once(&pred, ^{
_docsetManager = [[DHDocsetManager alloc] init];
[_docsetManager setUp];
});
return _docsetManager;
}
- (void)setUp
{
NSMutableArray *docsets = [NSMutableArray array];
NSFileManager *fileManager = [NSFileManager defaultManager];
for(NSDictionary *dictionary in [[NSUserDefaults standardUserDefaults] objectForKey:@"docsets"])
{
DHDocset *docset = [DHDocset docsetWithDictionaryRepresentation:dictionary];
if([fileManager fileExistsAtPath:docset.path])
{
[docsets addObject:docset];
}
}
self.docsets = docsets;
}
- (void)saveDefaults
{
NSMutableArray *dictionaries = [NSMutableArray array];
for(DHDocset *docset in self.docsets)
{
[dictionaries addObject:[docset dictionaryRepresentation]];
}
[[NSUserDefaults standardUserDefaults] setObject:dictionaries forKey:@"docsets"];
}
- (void)addDocset:(DHDocset *)docset andRemoveOthers:(BOOL)shouldRemove removeOnlyEqualPaths:(BOOL)removeOnlyEqualPaths
{
NSString *folder = [docset.path stringByDeletingLastPathComponent];
if(!docset)
{
return;
}
NSInteger index = [self.docsets indexOfObject:docset];
DHDocset *replaced = nil;
if(index != NSNotFound)
{
replaced = self.docsets[index];
[self.docsets removeObjectAtIndex:index];
}
if(shouldRemove)
{
NSIndexSet *toRemove = [self.docsets indexesOfObjectsPassingTest:^BOOL(DHDocset *obj, NSUInteger idx, BOOL *stop) {
if(!removeOnlyEqualPaths)
{
if([[obj.path stringByDeletingLastPathComponent] isCaseInsensitiveEqual:folder])
{
return YES;
}
}
else if([obj.path isCaseInsensitiveEqual:docset.path])
{
return YES;
}
return NO;
}];
if(index == NSNotFound && toRemove.count)
{
index = toRemove.firstIndex;
replaced = self.docsets[index];
}
[self.docsets removeObjectsAtIndexes:toRemove];
}
if(replaced)
{
[docset grabUserDataFromDocset:replaced];
[self.docsets insertObject:docset atIndex:index];
}
else
{
[self.docsets addObject:docset];
}
[self saveDefaults];
[[NSNotificationCenter defaultCenter] postNotificationName:DHDocsetsChangedNotification object:self];
}
- (void)removeDocsetsInFolder:(NSString *)path
{
[self.docsets removeObjectsAtIndexes:[self.docsets indexesOfObjectsPassingTest:^BOOL(DHDocset *obj, NSUInteger idx, BOOL *stop) {
if([[obj.path stringByDeletingLastPathComponent] isCaseInsensitiveEqual:path] || [obj.path isCaseInsensitiveEqual:path])
{
return YES;
}
return NO;
}]];
[self saveDefaults];
[[NSNotificationCenter defaultCenter] postNotificationName:DHDocsetsChangedNotification object:self];
}
- (void)moveDocsetAtIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex
{
DHDocset *toMove = self.docsets[fromIndex];
[self.docsets removeObjectAtIndex:fromIndex];
[self.docsets insertObject:toMove atIndex:toIndex];
[self saveDefaults];
}
- (DHDocset *)docsetForDocumentationPage:(NSString *)url
{
if([url hasPrefix:@"dash-apple-api://"])
{
return [self appleAPIReferenceDocset];
}
url = [[url stringByDeletingPathFragment] stringByReplacingPercentEscapes];
for(DHDocset *docset in [NSArray arrayWithArray:self.docsets])
{
NSString *path = docset.path;
if(path && [url rangeOfString:path].location != NSNotFound)
{
return docset;
}
}
return nil;
}
- (DHDocset *)docsetWithRelativePath:(NSString *)relativePath
{
for(DHDocset *docset in self.docsets)
{
if([docset.relativePath isEqualToString:relativePath])
{
return docset;
}
}
return nil;
}
- (NSMutableArray *)enabledDocsets
{
NSMutableArray *enabled = [NSMutableArray array];
for(DHDocset *docset in self.docsets)
{
if(docset.isEnabled)
{
[enabled addObject:docset];
}
}
return enabled;
}
- (DHDocset *)appleAPIReferenceDocset
{
NSMutableOrderedSet *toCheck = [NSMutableOrderedSet orderedSet];
[toCheck addObjectsFromArray:self.enabledDocsets];
[toCheck addObjectsFromArray:self.docsets];
for(DHDocset *docset in toCheck)
{
if([[[docset relativePath] lastPathComponent] isEqualToString:@"Apple_API_Reference.docset"] && [[NSFileManager defaultManager] fileExistsAtPath:[[docset documentsPath] stringByAppendingPathComponent:@"Apple Docs Helper"]] && ![[[docset plist] objectForKey:@"DashDocSetIsGeneratedForiOSCompatibility"] boolValue])
{
return docset;
}
}
return nil;
}
@end
================================================
FILE: Dash/DHDocsetTransferrer.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHRepo.h"
@interface DHDocsetTransferrer : DHRepo
@property (retain) NSTimer *pollTimer;
@property (retain) NSString *loadedFeedsHash;
@property (assign) NSInteger pollCounter;
@property (assign) BOOL ignoreReload;
+ (instancetype)sharedTransferrer;
- (IBAction)refreshFeeds:(id)sender;
@end
================================================
FILE: Dash/DHDocsetTransferrer.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetTransferrer.h"
#import "DHAppDelegate.h"
#import "DHTransferFeed.h"
@implementation DHDocsetTransferrer
static id singleton = nil;
- (void)setUp // doesn't get called unless you call the singleton from DHAppDelegate
{
[super setUp];
}
- (void)feedWillInstall:(DHTransferFeed *)feed
{
feed.feedResult = [[DHFeedResult alloc] init];
feed.feedResult.feed = feed;
NSString *sourcePath = [transfersPath stringByAppendingPathComponent:feed.feed];
@synchronized([DHDocsetTransferrer class])
{
feed.feedURL = [self docsetPathForFeed:feed];
NSFileManager *fileManager = [NSFileManager defaultManager];
if([fileManager fileExistsAtPath:feed.feedURL])
{
NSString *trashPath = [self uniqueTrashPath];
[fileManager moveItemAtPath:feed.feedURL toPath:trashPath error:nil];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[[NSFileManager defaultManager] removeItemAtPath:trashPath error:nil];
});
}
[fileManager createDirectoryAtPath:feed.feedURL.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil];
[fileManager moveItemAtPath:sourcePath toPath:feed.feedURL error:nil];
feed.docset = nil;
[feed loadDocset];
}
}
- (NSString *)installFeed:(DHTransferFeed *)feed isAnUpdate:(BOOL)isAnUpdate
{
DHFeedResult *result = feed.feedResult;
DHDocset *docset = feed.docset;
if(!result || result.isCancelled)
{
return @"cancelled";
}
if(!docset || !docset.optimisedIndexPath)
{
return @"Couldn't load docset. Either iTunes did not finish transferring it or the docset is corrupt.";
}
CGFloat maxWidth = [DHRightDetailLabel calculateMaxDetailWidthBasedOnLongestPossibleString:@"Indexing..."];
[feed setMaxRightDetailWidth:maxWidth];
[[[feed cell] titleLabel] setMaxRightDetailWidth:maxWidth];
NSString *tempPath = [docset.optimisedIndexPath stringByAppendingString:@"_temp"];
docset.tempOptimisedIndexPath = tempPath;
[feed.feedResult setRightDetail:@"Waiting..."];
@synchronized([DHDocsetIndexer class])
{
[DHDocsetIndexer indexerForDocset:docset delegate:feed.feedResult];
}
if(result.isCancelled)
{
return @"cancelled";
}
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager moveItemAtPath:tempPath toPath:docset.optimisedIndexPath error:nil];
feed.docset.tempOptimisedIndexPath = nil;
return nil;
}
- (void)feedWasStopped:(DHTransferFeed *)feed
{
@synchronized([DHDocsetTransferrer class])
{
[feed cancelInstall];
}
}
- (void)feedDidUninstall:(DHTransferFeed *)feed
{
NSInteger row = [self.feeds indexOfObjectIdenticalTo:feed];
if(row != NSNotFound)
{
[self.feeds removeObjectAtIndex:row];
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:row inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
for(NSInteger i = row; i < self.feeds.count; i++)
{
DHTransferFeed *nextFeed = self.feeds[i];
[nextFeed.cell setTagsToIndex:i];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if(!self.feeds.count)
{
[self reload];
}
});
}
else
{
[self refreshFeeds:nil];
}
}
- (IBAction)refreshFeeds:(id)sender
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *waitingPath = transfersPath;
NSArray *waiting = [fileManager contentsOfDirectoryAtPath:waitingPath error:nil];
self.loadedFeedsHash = [waiting componentsJoinedByString:@"xx"];
NSMutableArray *newFeeds = [NSMutableArray array];
for(NSString *docset in waiting)
{
if([docset hasCaseInsensitiveSuffix:@".docset"])
{
DHTransferFeed *feed = [DHTransferFeed feedWithPath:[waitingPath stringByAppendingPathComponent:docset] isInstalled:NO];
[newFeeds addObject:feed];
}
}
NSString *installedPath = [self docsetInstallFolderPath];
for(NSString *docset in [fileManager contentsOfDirectoryAtPath:installedPath error:nil])
{
if([docset hasCaseInsensitiveSuffix:@".docset"])
{
DHTransferFeed *feed = [DHTransferFeed feedWithPath:[installedPath stringByAppendingPathComponent:docset] isInstalled:YES];
if(![newFeeds containsObject:feed])
{
if(!self.feeds && ![feed isProperlyInstalled])
{
[feed cancelInstall];
}
[newFeeds addObject:feed];
}
}
}
if(!self.feeds)
{
self.feeds = [NSMutableArray array];
}
NSMutableArray *oldFeeds = [NSMutableArray arrayWithArray:self.feeds];
@synchronized([DHDocsetTransferrer class])
{
for(DHTransferFeed *newFeed in newFeeds)
{
NSInteger index = [oldFeeds indexOfObject:newFeed];
if(index != NSNotFound)
{
DHTransferFeed *feed = oldFeeds[index];
if((feed.installed || feed.installing) && !newFeed.installed)
{
if(feed.installing)
{
[feed cancelInstall];
}
if(!newFeed.docset)
{
[newFeed loadDocset];
}
oldFeeds[index] = newFeed;
}
else
{
[oldFeeds[index] refreshIcon];
}
}
else
{
if(!newFeed.docset)
{
[newFeed loadDocset];
}
[oldFeeds addObject:newFeed];
}
}
for(DHTransferFeed *feed in [NSMutableArray arrayWithArray:oldFeeds])
{
if(![newFeeds containsObject:feed])
{
[oldFeeds removeObjectIdenticalTo:feed];
}
}
}
[oldFeeds sortUsingFunction:compareFeeds context:nil];
self.feeds = oldFeeds;
[self reload];
}
- (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView
{
NSString *text = @"Transfer Docsets";
return [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: [UIFont fontWithName:@"HelveticaNeue-Light" size:22.0], NSForegroundColorAttributeName: [UIColor colorWithWhite:201.0/255.0 alpha:1.0]}];
}
- (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView {
NSString *text = @"You can transfer docsets using iTunes File Sharing or AirDrop.\n\nFor best results, docsets that are available for download should always be downloaded instead of transferred.";
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
paragraph.lineBreakMode = NSLineBreakByWordWrapping;
paragraph.alignment = NSTextAlignmentCenter;
paragraph.lineSpacing = 4.0;
NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:13.0], NSForegroundColorAttributeName: [UIColor colorWithWhite:207.0/255.0 alpha:1.0], NSParagraphStyleAttributeName: paragraph};
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}
- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView
{
return [UIImage imageNamed:@"placeholder_transfer"];
}
- (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state
{
NSString *text = @"Open Help";
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16.0], NSForegroundColorAttributeName: [[DHAppDelegate sharedDelegate].window.rootViewController.view.tintColor colorWithAlphaComponent:state == UIControlStateNormal ? 0.7 : 0.2]};
return [[NSAttributedString alloc] initWithString:text attributes:attributes];
}
- (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView
{
return NO;
}
- (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView
{
return 24;
}
- (void)emptyDataSetWillAppear:(UIScrollView *)scrollView
{
[self.tableView setContentOffset:CGPointMake(0, -self.tableView.contentInset.top)];
}
- (void)emptyDataSetDidTapButton:(UIScrollView *)scrollView
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://kapeli.com/dash_itunes_file_sharing"]];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.tableFooterView = [UIView new];
self.tableView.emptyDataSetSource = self;
self.tableView.emptyDataSetDelegate = self;
self.ignoreReload = YES;
[self poll];
self.ignoreReload = NO;
if(self.feeds.count)
{
[self reload];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
}
- (void)orientationChanged:(id)sender
{
[self.tableView reloadEmptyDataSet];
}
- (void)poll
{
++self.pollCounter;
NSFileManager *fileManager = [NSFileManager defaultManager];
if(!self.loadedFeedsHash || ![[[fileManager contentsOfDirectoryAtPath:transfersPath error:nil] componentsJoinedByString:@"xx"] isEqualToString:self.loadedFeedsHash])
{
[self refreshFeeds:nil];
}
else
{
if(self.pollCounter % 3 == 0)
{
BOOL shouldReload = NO;
for(DHTransferFeed *feed in self.feeds)
{
if(!feed.docset && [feed loadDocset])
{
shouldReload = YES;
}
else if(feed.docset)
{
shouldReload |= [feed refreshIcon];
}
}
if(shouldReload)
{
[self.feeds sortUsingFunction:compareFeeds context:nil];
[self reload];
}
self.pollCounter = 0;
}
}
}
- (void)reload
{
if(self.ignoreReload)
{
return;
}
NSInteger count = [self tableView:self.tableView numberOfRowsInSection:0];
self.tableView.tableFooterView = (count) ? nil : [UIView new];
[self.tableView reloadData];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(stopPollTimer) name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startPollTimer) name:UIApplicationDidBecomeActiveNotification object:nil];
[self poll];
[self startPollTimer];
}
- (void)viewWillDisappear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
[super viewWillDisappear:animated];
[self stopPollTimer];
self.pollTimer = [self.pollTimer invalidateTimer];
}
- (void)startPollTimer
{
[self stopPollTimer];
self.pollTimer = [NSTimer scheduledTimerWithTimeInterval:2.0f target:self selector:@selector(poll) userInfo:nil repeats:YES];
}
- (void)stopPollTimer
{
self.pollTimer = [self.pollTimer invalidateTimer];
}
- (NSString *)docsetInstallFolderName
{
return @"Transfers";
}
- (NSString *)docsetPathForFeed:(DHFeed *)feed
{
return [[self docsetInstallFolderPath] stringByAppendingPathComponent:feed.feed];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DHRepoTableViewCell *cell = (id)[super tableView:tableView cellForRowAtIndexPath:indexPath];
[cell.downloadButton setImage:[UIImage imageNamed:@"transfer_button"] forState:UIControlStateNormal];
[cell.downloadButton setBackgroundImage:nil forState:UIControlStateNormal];
return cell;
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
return nil;
}
- (IBAction)errorButtonPressed:(id)sender
{
NSUInteger row = [sender tag];
DHFeed *feed = [self activeFeeds][row];
[[[UIAlertView alloc] initWithTitle:@"Docset Install Failed" message:feed.error delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
+ (instancetype)sharedTransferrer
{
if(singleton)
{
return singleton;
}
id transferrer = [[DHAppDelegate mainStoryboard] instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
[transferrer setUp];
return transferrer;
}
+ (id)alloc
{
if(singleton)
{
return singleton;
}
return [super alloc];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if(singleton)
{
return singleton;
}
self = [super initWithCoder:aDecoder];
singleton = self;
return self;
}
@end
================================================
FILE: Dash/DHEntryBrowser.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetBrowser.h"
@interface DHEntryBrowser : UITableViewController
@property (strong) DHDocset *docset;
@property (strong) NSString *type;
@property (strong) NSMutableArray *entries;
@property (assign) BOOL isLoading;
@property (assign) BOOL isEmpty;
@property (strong) DHDBSearchController *searchController;
@property (strong) NSCoder *decoder;
@property (assign) BOOL isRestoring;
@property (assign) BOOL didLoad;
@end
================================================
FILE: Dash/DHEntryBrowser.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHEntryBrowser.h"
#import "DHTypes.h"
#import "DHDBResult.h"
#import "DHDocsetManager.h"
@implementation DHEntryBrowser
- (void)viewDidLoad
{
if(!self.docset)
{
// happens during state restoration
return;
}
[super viewDidLoad];
self.clearsSelectionOnViewWillAppear = NO;
self.searchController = [DHDBSearchController searchControllerWithDocsets:@[self.docset] typeLimit:self.type viewController:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareForURLSearch:) name:DHPrepareForURLSearch object:nil];
[self.tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
[self.tableView registerNib:[UINib nibWithNibName:@"DHLoadingCell" bundle:nil] forCellReuseIdentifier:@"DHLoadingCell"];
self.tableView.rowHeight = 44;
if(self.isRestoring)
{
return;
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!self.didLoad && self.docset)
{
self.didLoad = YES;
self.tableView.allowsSelection = NO;
self.isLoading = YES;
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[self.docset executeBlockWithinDocsetDBConnection:^(FMDatabase *db) {
NSMutableSet *duplicates = [NSMutableSet set];
NSMutableArray *entries = [NSMutableArray array];
NSConditionLock *lock = [DHDocset stepLock];
[lock lockWhenCondition:DHLockAllAllowed];
FMResultSet *rs = [db executeQuery:@"SELECT path, name, type FROM searchIndex WHERE type = ? ORDER BY LOWER(name)", self.type];
BOOL next = [rs next];
[lock unlock];
while(next)
{
DHDBResult *result = [DHDBResult resultWithDocset:self.docset resultSet:rs];
if(result)
{
NSString *duplicateHash = [result browserDuplicateHash];
if(!duplicateHash || ![duplicates containsObject:duplicateHash])
{
if(duplicateHash)
{
[duplicates addObject:duplicateHash];
}
[entries addObject:result];
}
}
[lock lockWhenCondition:DHLockAllAllowed];
next = [rs next];
[lock unlock];
}
dispatch_sync(dispatch_get_main_queue(), ^{
self.isLoading = NO;
self.isEmpty = entries.count <= 0;
if(!self.isEmpty)
{
self.tableView.allowsSelection = YES;
}
self.entries = entries;
[self.tableView reloadData];
});
} readOnly:YES lockCondition:DHLockAllAllowed optimisedIndex:YES];
});
}
[self.tableView deselectAll:YES];
[self.searchController viewWillAppear];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
[self.tableView reloadData];
[self.searchController traitCollectionDidChange:previousTraitCollection];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.searchController viewWillDisappear];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.searchController viewDidDisappear];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.searchController viewDidAppear];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(self.isLoading || self.isEmpty)
{
return 3;
}
NSInteger count = self.entries.count;
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DHBrowserTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:(self.isLoading || self.isEmpty) ? @"DHLoadingCell" : @"DHBrowserCell" forIndexPath:indexPath];
if((self.isLoading || self.isEmpty) && indexPath.row == 2)
{
cell.userInteractionEnabled = NO;
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraph setAlignment:NSTextAlignmentCenter];
UIFont *font = [UIFont boldSystemFontOfSize:20];
cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:(self.isEmpty) ? @"Nothing Here" : @"Loading..." attributes:@{NSParagraphStyleAttributeName : paragraph, NSForegroundColorAttributeName: [UIColor colorWithWhite:0.8 alpha:1], NSFontAttributeName: font}];
}
else if(self.isLoading || self.isEmpty)
{
cell.userInteractionEnabled = NO;
cell.textLabel.text = @"";
}
else
{
cell.userInteractionEnabled = YES;
DHDBResult *entry = self.entries[indexPath.row];
cell.textLabel.font = [UIFont fontWithName:@"Menlo" size:16];
cell.textLabel.text = entry.originalName;
cell.imageView.image = [UIImage imageNamed:self.type];
cell.accessoryType = (entry.similarResults.count || !isRegularHorizontalClass) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
DHDBResult *result = self.entries[indexPath.row];
[[DHDBResultSorter sharedSorter] resultWasSelected:result inTableView:tableView];
if(isRegularHorizontalClass)
{
[[DHWebViewController sharedWebViewController] loadResult:result];
}
else
{
[[DHWebViewController sharedWebViewController] loadResult:result];
[self performSegueWithIdentifier:@"DHWebViewSegue" sender:self];
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:@"DHWebViewSegue"])
{
DHWebViewController *webViewController = [segue destinationViewController];
DHDBResult *selectedEntry = self.entries[self.tableView.indexPathForSelectedRow.row];
webViewController.result = selectedEntry;
}
else
{
[self.searchController prepareForSegue:segue sender:sender];
}
}
- (void)prepareForURLSearch:(id)sender
{
[self.searchDisplayController setActive:NO animated:NO];
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[coder encodeObject:self.docset.relativePath forKey:@"docsetRelativePath"];
[coder encodeObject:self.type forKey:@"type"];
[coder encodeObject:self.title forKey:@"title"];
[coder encodeObject:self.entries forKey:@"entries"];
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if(selectedIndexPath != nil)
{
[coder encodeObject:selectedIndexPath forKey:@"selectedIndexPath"];
}
[self.searchController encodeRestorableStateWithCoder:coder];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
NSString *docsetRelativePath = [coder decodeObjectForKey:@"docsetRelativePath"];
self.docset = [[DHDocsetManager sharedManager] docsetWithRelativePath:docsetRelativePath];
self.type = [coder decodeObjectForKey:@"type"];
self.title = [coder decodeObjectForKey:@"title"];
self.isRestoring = YES;
[self viewDidLoad];
self.isRestoring = NO;
self.entries = [coder decodeObjectForKey:@"entries"];
for (int i = 0; i < self.entries.count; i++) {
if ([self.entries[i] isKindOfClass:[DHDBResult class]]) {
DHDBResult *result = (DHDBResult *)self.entries[i];
NSArray *components = [result.fullPath componentsSeparatedByString:@"/Library/Docsets"];
if (components.count == 2) {
result.fullPath = [@"file://" stringByAppendingFormat:@"%@%@%@",homePath,@"/Docsets",components[1]];
}
}
}
NSIndexPath *selectedIndexPath = [coder decodeObjectForKey:@"selectedIndexPath"];
if(selectedIndexPath != nil)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
});
}
[self.searchController decodeRestorableStateWithCoder:coder];
[super decodeRestorableStateWithCoder:coder];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
================================================
FILE: Dash/DHFeed.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHRepoTableViewCell.h"
#import "DHFeedResult.h"
@interface DHFeed : NSObject
@property (strong) NSString *name;
@property (strong) NSString *_uniqueIdentifier;
@property (strong) NSString *feed;
@property (strong) NSString *feedURL;
@property (strong) NSString *iconName;
@property (strong) NSArray *aliases;
@property (strong) NSString *size;
@property (strong) NSString *platform;
@property (strong) NSString *authorLinkText;
@property (strong) NSString *authorLinkHref;
@property (assign) BOOL doesNotHaveVersions;
@property (assign) BOOL waiting;
@property (assign) BOOL isCustom;
@property (assign) BOOL progressShown;
@property (assign) BOOL checkingForUpdates;
@property (assign) BOOL installing, installed;
@property (assign) BOOL _isMajorVersioned;
@property (strong) NSString *installedVersion;
@property (assign) float progress;
@property (retain) NSString *detailString;
@property (strong) NSString *error;
@property (strong) DHFeedResult *feedResult;
@property (retain) NSObject *identifier;
@property (weak) DHRepoTableViewCell *cell;
@property (assign) CGFloat maxRightDetailWidth;
@property (strong) UIImage *_icon;
+ (instancetype)entryWithName:(NSString *)name platform:(NSString *)platform icon:(UIImage *)icon;
+ (instancetype)feedWithFeed:(NSString *)aFeed icon:(NSString *)aIcon aliases:(id)someAliases doesNotHaveVersions:(BOOL)doesNotHaveVersions;
+ (DHFeed *)feedWithDictionaryRepresentation:(NSDictionary *)dictionary;
- (NSDictionary *)dictionaryRepresentation;
- (NSString *)docsetNameWithVersion:(BOOL)withVersion;
- (NSString *)stringValue;
- (void)prepareCell:(DHRepoTableViewCell *)cell;
- (NSString *)sortName;
- (void)adjustTitleLabelWidthBasedOnButtonsShown;
- (UIImage *)icon;
- (NSString *)uniqueIdentifier; // Used to find a corresponding feed from a installed docset
- (NSString *)installFolderName;
@end
================================================
FILE: Dash/DHFeed.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHFeed.h"
@implementation DHFeed
+ (instancetype)entryWithName:(NSString *)name platform:(NSString *)platform icon:(UIImage *)icon
{
DHFeed *feed = [[DHFeed alloc] init];
feed._icon = icon;
feed.name = name;
feed.platform = platform;
return feed;
}
// For Dash provided feeds only
+ (instancetype)feedWithFeed:(NSString *)aFeed icon:(NSString *)aIcon aliases:(id)someAliases doesNotHaveVersions:(BOOL)doesNotHaveVersions
{
DHFeed *feed = [[DHFeed alloc] init];
feed.detailString = @"";
feed.feed = aFeed;
feed.iconName = aIcon;
NSString *feedURL = [@"http://kapeli.com/feeds/" stringByAppendingString:aFeed];
if([aFeed isEqualToString:@"SproutCore.xml"])
{
feed.isCustom = YES;
feedURL = [@"http://docs.sproutcore.com/feeds/" stringByAppendingString:aFeed];
}
feed.feedURL = feedURL;
feed.aliases = ([someAliases isKindOfClass:[NSArray class]]) ? someAliases : (someAliases) ? @[someAliases] : nil;
feed.doesNotHaveVersions = doesNotHaveVersions;
return feed;
}
- (NSDictionary *)dictionaryRepresentation
{
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
dictionary[@"feed"] = self.feed ?: @"";
dictionary[@"platform"] = self.platform ?: @"";
dictionary[@"name"] = self.name ?: @"";
dictionary[@"feedURL"] = self.feedURL ?: @"";
dictionary[@"_uniqueIdentifier"] = self._uniqueIdentifier ?: @"";
dictionary[@"icon"] = self.iconName ?: @"";
dictionary[@"size"] = self.size ?: @"";
dictionary[@"aliases"] = self.aliases ?: @[];
dictionary[@"doesNotHaveVersions"] = self.doesNotHaveVersions ? @YES : @NO;
dictionary[@"installed"] = self.installed ? @YES : @NO;
dictionary[@"isCustom"] = self.isCustom ? @YES : @NO;
dictionary[@"_isMajorVersioned"] = self._isMajorVersioned ? @YES : @NO;
if(self.installedVersion) { dictionary[@"installedVersion"] = self.installedVersion; }
dictionary[@"authorLinkHref"] = self.authorLinkHref ?: @"";
dictionary[@"authorLinkText"] = self.authorLinkText ?: @"";
return dictionary;
}
+ (DHFeed *)feedWithDictionaryRepresentation:(NSDictionary *)dictionary
{
DHFeed *feed = [[DHFeed alloc] init];
feed.detailString = @"";
feed.platform = dictionary[@"platform"];
feed.name = dictionary[@"name"];
feed.feed = dictionary[@"feed"];
feed.feedURL = dictionary[@"feedURL"];
feed._uniqueIdentifier = dictionary[@"_uniqueIdentifier"];
feed.size = dictionary[@"size"];
feed.iconName = dictionary[@"icon"];
feed.aliases = dictionary[@"aliases"];
feed.doesNotHaveVersions = [dictionary[@"doesNotHaveVersions"] boolValue];
feed.installed = [dictionary[@"installed"] boolValue];
feed._isMajorVersioned = [dictionary[@"_isMajorVersioned"] boolValue];
feed.installedVersion = dictionary[@"installedVersion"];
feed.authorLinkHref = dictionary[@"authorLinkHref"];
feed.authorLinkText = dictionary[@"authorLinkText"];
feed.isCustom = [dictionary[@"isCustom"] boolValue];
return feed;
}
- (NSString *)docsetNameWithVersion:(BOOL)withVersion
{
NSString *docsetName = (self.name) ? self.name : [NSString stringWithFormat:@"%@", [[[[self.feedURL lastPathComponent] stringByDeletingPathExtension] stringByReplacingOccurrencesOfString:@"_" withString:@" "] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]];
if([docsetName isEqualToString:@"NET Framework"])
{
docsetName = @".NET Framework";
}
else if([docsetName isEqualToString:@"Angular.dart"])
{
docsetName = @"AngularDart";
}
else if([docsetName isEqualToString:@"MatPlotLib"])
{
docsetName = @"Matplotlib";
}
else if([docsetName isEqualToString:@"Lo-Dash"])
{
docsetName = @"Lodash";
}
if(withVersion && self.installed && ![self.platform isEqualToString:@"cheatsheet"])
{
NSString *version = self.installedVersion;
version = [version substringToString:@"/"];
if([version length])
{
if([self isMajorVersioned:self.feedURL])
{
docsetName = [docsetName stringByAppendingFormat:@"%@", version];
}
else
{
docsetName = [docsetName stringByAppendingFormat:@" %@", version];
}
}
}
return docsetName;
}
- (BOOL)isMajorVersioned:(NSString *)feedURL
{
if(self._isMajorVersioned)
{
return YES;
}
return [feedURL isEqualToString:@"http://kapeli.com/feeds/Drupal_7.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Drupal_8.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Zend_Framework_1.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Zend_Framework_2.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Zend_Framework_3.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Bootstrap_2.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Bootstrap_3.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Bootstrap_4.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Python_2.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Python_3.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Java_SE6.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Java_SE7.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Java_SE8.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Java_SE9.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Java_SE10.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Java_SE11.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Ruby_on_Rails_3.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Ruby_on_Rails_4.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Ruby_on_Rails_5.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Qt_4.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Qt_5.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Lua_5.1.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Lua_5.2.xml"] || [feedURL isEqualToString:@"http://kapeli.com/feeds/Ruby_2.xml"];
}
- (NSString *)sortName
{
NSString *name = [self docsetNameWithVersion:YES];
if([name hasPrefix:@"Angular.dart"])
{
return [name stringByReplacingOccurrencesOfString:@"Angular.dart" withString:@"Angularz.dart"];
}
else if([name hasPrefix:@"Java SE10"])
{
return [name stringByReplacingOccurrencesOfString:@"Java SE10" withString:@"Java SEz"];
}
return name;
}
- (NSString *)stringValue
{
return [self docsetNameWithVersion:NO];
}
- (void)prepareCell:(DHRepoTableViewCell *)cell
{
self.cell = cell;
if(self.progressShown)
{
cell.progressView.alpha = 1.0;
cell.downloadButton.alpha = 0.0;
cell.uninstallButton.alpha = 0.0;
cell.checkmark.alpha = 0.0;
cell.progressView.progress = self.progress;
}
else
{
cell.progressView.alpha = 0.0;
if(self.installed)
{
cell.downloadButton.alpha = 0.0;
cell.uninstallButton.alpha = 1.0;
cell.checkmark.alpha = 1.0;
}
else
{
cell.downloadButton.alpha = 1.0;
cell.uninstallButton.alpha = 0.0;
cell.checkmark.alpha = 0.0;
}
}
if(self.error.length && !self.installing && !self.installed)
{
cell.errorButton.alpha = 1.0;
}
else
{
cell.errorButton.alpha = 0.0;
}
[self adjustTitleLabelWidthBasedOnButtonsShown];
}
- (void)adjustTitleLabelWidthBasedOnButtonsShown
{
CGFloat endX = self.cell.downloadButton.frame.origin.x;
if(self.cell.errorButton.alpha > 0.0)
{
endX = self.cell.errorButton.frame.origin.x;
}
else if(self.cell.uninstallButton.alpha > 0.0)
{
int offset = (isRegularHorizontalClass) ? 3 : 0;
endX = self.cell.errorButton.frame.origin.x+offset;
}
endX -= 6;
CGRect frame = self.cell.titleLabel.frame;
self.cell.titleLabel.frame = CGRectMake(frame.origin.x, frame.origin.y, endX-frame.origin.x, frame.size.height);
}
- (UIImage *)icon
{
if(self._icon)
{
return self._icon;
}
UIImage *image = [UIImage imageNamed:self.iconName];
return (image) ? image : [UIImage imageNamed:@"Other"];
}
- (BOOL)isEqual:(id)object
{
return [self.uniqueIdentifier isEqualToString:[object uniqueIdentifier]];
}
- (NSString *)uniqueIdentifier // Used to find a corresponding feed from a installed docset
{
if(self._uniqueIdentifier.length)
{
return self._uniqueIdentifier;
}
return self.feedURL;
}
- (NSString *)installFolderName
{
return (self._uniqueIdentifier.length) ? self._uniqueIdentifier.lastPathComponent : self.feed.lastPathComponent.stringByDeletingPathExtension;
}
@end
================================================
FILE: Dash/DHFeedResult.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHFileDownload.h"
@interface DHFeedResult : NSObject
@property (strong) NSArray *downloadURLs;
@property (strong) NSString *version;
@property (assign) NSUInteger expectedContentLength;
@property (assign) NSUInteger receivedContentLength;
@property (strong) DHFileDownload *fileDownload;
@property (weak) id feed;
@property (strong) NSDate *lastDownloadProgressUpdate;
@property (assign) double lastProgress;
@property (assign, nonatomic) BOOL hasTarix;
- (BOOL)isCancelled;
- (void)setUnarchiveProgress:(double)progress;
- (void)setIndexingProgress:(double)progress;
- (void)setDownloadProgress:(double)progress receivedBytes:(long long)length outOf:(long long)expectedLength;
- (void)setRightDetail:(NSString *)rightDetail;
@end
================================================
FILE: Dash/DHFeedResult.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHFeedResult.h"
#import "DHFeed.h"
#import "DHTransferFeed.h"
@implementation DHFeedResult
- (void)setUnarchiveProgress:(double)progress
{
if(![self isCancelled])
{
progress = (self.hasTarix) ? 0.75 : 0.4+progress/2.0/1.25;
dispatch_sync(dispatch_get_main_queue(), ^{
[self.feed setProgress:progress];
[[[self.feed cell] progressView] setProgress:progress animated:YES];
});
}
}
- (void)setIndexingProgress:(double)progress
{
if(![self isCancelled])
{
progress = ([self.feed isKindOfClass:[DHTransferFeed class]]) ? progress : (self.hasTarix) ? 0.75 + progress/4 : 0.8+progress/5;
dispatch_sync(dispatch_get_main_queue(), ^{
[self.feed setProgress:progress];
[[[self.feed cell] progressView] setProgress:progress animated:YES];
});
}
}
- (void)setDownloadProgress:(double)progress receivedBytes:(long long)length outOf:(long long)expectedLength
{
if(![self isCancelled])
{
double unalteredProgress = progress;
progress = (self.hasTarix) ? progress/2*1.5 : progress/2/1.25;
[self.feed setProgress:progress];
[[[self.feed cell] progressView] setProgress:progress animated:YES];
if(expectedLength != -1)
{
NSDate *lastNameUpdate = self.lastDownloadProgressUpdate;
NSDate *now = [NSDate date];
if(!lastNameUpdate || [now timeIntervalSinceDate:lastNameUpdate] > 1.0 || fabs(self.lastProgress - unalteredProgress) > 0.05 || unalteredProgress == 1.0)
{
if(!lastNameUpdate)
{
NSString *maxString = [NSString stringByFormattingDownloadProgress:expectedLength totalBytes:expectedLength];
CGFloat maxWidth = [DHRightDetailLabel calculateMaxDetailWidthBasedOnLongestPossibleString:maxString];
[self.feed setMaxRightDetailWidth:maxWidth];
[(DHFeed*)self.feed setSize:[maxString substringFromString:@"/"]];
[[[self.feed cell] titleLabel] setMaxRightDetailWidth:maxWidth];
}
self.lastDownloadProgressUpdate = now;
self.lastProgress = unalteredProgress;
if(expectedLength > 0)
{
NSString *progressString = [NSString stringByFormattingDownloadProgress:length totalBytes:expectedLength];
[self setRightDetail:progressString];
}
}
}
}
}
- (void)setRightDetail:(NSString *)rightDetail
{
if(![NSThread isMainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
[self setRightDetail:rightDetail];
});
return;
}
[self.feed setDetailString:rightDetail];
[self.feed cell].titleLabel.rightDetailText = rightDetail;
}
- (BOOL)isCancelled
{
return ![self.feed installing] || [self.feed feedResult] != self;
}
@end
================================================
FILE: Dash/DHFileDownload.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHFileDownload : NSObject
@property (nonatomic) NSURLSession *session;
@property (nonatomic) NSURLSessionTask *downloadTask;
@property (retain) NSError *error;
@property (retain) NSString *filePath;
@property (assign) BOOL isDone;
@property (retain) id delegate;
@property (retain) id identifier;
@property (assign) BOOL cancelled;
+ (BOOL)downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError **)error delegate:(id)delegate identifier:(id)identifier;
- (void)cancelDownload;
@end
#define DHDownloadCancelled 2481939
================================================
FILE: Dash/DHFileDownload.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHFileDownload.h"
#import "DHFeedResult.h"
@implementation DHFileDownload
+ (BOOL)downloadItemAtURL:(NSURL *)url toFile:(NSString *)localPath error:(NSError **)error delegate:(id)delegate identifier:(id)identifier
{
NSString *newURL = [[url absoluteString] stringByConvertingKapeliHttpURLToHttpsReturningNil];
if(newURL.length)
{
url = [NSURL URLWithString:newURL];
}
if(![NSURL URLIsFound:[url absoluteString] timeoutInterval:120.0 checkForRedirect:NO])
{
return NO;
}
DHFileDownload *fileDownload = [[DHFileDownload alloc] init];
if(identifier && [identifier isKindOfClass:[DHFeedResult class]])
{
[identifier setExpectedContentLength:0];
[identifier setReceivedContentLength:0];
[identifier setFileDownload:fileDownload];
}
fileDownload.identifier = identifier;
fileDownload.delegate = delegate;
fileDownload.filePath = localPath;
NSFileManager *fileManager = [NSFileManager defaultManager];
if([fileManager fileExistsAtPath:localPath])
{
[fileManager removeItemAtPath:localPath error:nil];
}
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[NSString stringWithFormat:@"%@%u", [url absoluteString], arc4random() % 100000]];
configuration.timeoutIntervalForRequest = 900;
configuration.HTTPAdditionalHeaders = @{@"User-Agent": [[NSBundle mainBundle] bundleIdentifier]};
fileDownload.session = [NSURLSession sessionWithConfiguration:configuration delegate:fileDownload delegateQueue:nil];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:900.0];
fileDownload.downloadTask = [fileDownload.session downloadTaskWithRequest:request];
[fileDownload.downloadTask resume];
while(!fileDownload.isDone)
{
[NSThread sleepForTimeInterval:0.1f];
}
if(identifier && [identifier isKindOfClass:[DHFeedResult class]])
{
[identifier setFileDownload:nil];
}
fileDownload.identifier = nil;
fileDownload.delegate = nil;
fileDownload.downloadTask = nil;
if(fileDownload.error != nil)
{
if(error != nil)
{
*error = fileDownload.error;
}
[fileDownload.session invalidateAndCancel];
fileDownload.session = nil;
return NO;
}
[fileDownload.session invalidateAndCancel];
fileDownload.session = nil;
dispatch_sync(dispatch_get_main_queue(), ^{
[identifier setDownloadProgress:1.0 receivedBytes:-1 outOf:-1];
});
return YES;
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
if(totalBytesExpectedToWrite > 0)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.identifier setDownloadProgress:(double)totalBytesWritten/totalBytesExpectedToWrite receivedBytes:totalBytesWritten outOf:totalBytesExpectedToWrite];
});
}
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadURL
{
if(downloadURL && [downloadURL path] && [[NSFileManager defaultManager] fileExistsAtPath:[downloadURL path]])
{
[[NSFileManager defaultManager] moveItemAtPath:[downloadURL path] toPath:self.filePath error:nil];
}
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
NSLog(@"resumed at offset!?");
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
self.isDone = YES;
if(error)
{
if(!self.cancelled)
{
self.error = error;
}
}
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
}
- (void)cancelDownload
{
self.cancelled = YES;
self.error = [NSError errorWithDomain:@"com.kapeli.dash" code:DHDownloadCancelled userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%d", DHDownloadCancelled]}];
[self.identifier setExpectedContentLength:0];
[self.identifier setReceivedContentLength:0];
self.isDone = YES;
[self.downloadTask cancel];
}
@end
================================================
FILE: Dash/DHImageCache.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHImageCache : NSObject
@property (strong) NSMutableDictionary *filesCache;
+ (DHImageCache *)sharedCache;
+ (UIImage *)imageWithContentsOfFile:(NSString *)path fullRefresh:(BOOL)fullRefresh;
@end
================================================
FILE: Dash/DHImageCache.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHImageCache.h"
@implementation DHImageCache
+ (DHImageCache *)sharedCache
{
static dispatch_once_t pred;
static DHImageCache *_imageCache = nil;
dispatch_once(&pred, ^{
_imageCache = [[DHImageCache alloc] init];
_imageCache.filesCache = [NSMutableDictionary dictionary];
});
return _imageCache;
}
+ (UIImage *)imageWithContentsOfFile:(NSString *)path fullRefresh:(BOOL)fullRefresh
{
if(!path || !path.length)
{
return nil;
}
DHImageCache *cache = [DHImageCache sharedCache];
if(fullRefresh)
{
[cache.filesCache removeObjectForKey:path];
}
id image = (cache.filesCache)[path];
if(image == [NSNull null])
{
return nil;
}
else if(image)
{
return image;
}
else
{
BOOL exists = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *retinaPath = [[path stringByDeletingPathExtension] stringByAppendingString:@"@2x.png"];
if([[path pathExtension] isEqualToString:@"png"] && [fileManager fileExistsAtPath:retinaPath])
{
exists = YES;
path = retinaPath;
}
if(exists || [fileManager fileExistsAtPath:path])
{
image = [UIImage imageWithContentsOfFile:path];
}
if(image)
{
(cache.filesCache)[path] = image;
}
else
{
(cache.filesCache)[path] = [NSNull null];
}
return image;
}
return nil;
}
@end
================================================
FILE: Dash/DHJavaScript.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHJavaScript : NSObject
@property (strong) NSMutableDictionary *javaScripts;
+ (DHJavaScript *)sharedJavaScript;
- (NSString *)javaScriptInFile:(NSString *)file;
- (NSString *)zoomScriptWithFrame:(CGRect)frame;
- (NSString *)injectCSSScript;
- (NSString *)injectViewPortScript;
@end
================================================
FILE: Dash/DHJavaScript.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHJavaScript.h"
#import "DHCSS.h"
#import "DHWebView.h"
#import "DHWebViewController.h"
@implementation DHJavaScript
+ (DHJavaScript *)sharedJavaScript
{
static dispatch_once_t pred;
static DHJavaScript *_javaScript = nil;
dispatch_once(&pred, ^{
_javaScript = [[DHJavaScript alloc] init];
_javaScript.javaScripts = [[NSMutableDictionary alloc] init];
});
return _javaScript;
}
- (NSString *)javaScriptInFile:(NSString *)file
{
if(self.javaScripts[file])
{
return self.javaScripts[file];
}
NSString *javaScript = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
self.javaScripts[file] = javaScript;
return javaScript;
}
- (NSString *)zoomScriptWithFrame:(CGRect)frame
{
return [NSString stringWithFormat:@"document.getElementById('dash_viewport').setAttribute('content', 'width=%ld, initial-scale=1');", (long)frame.size.width];
}
- (NSString *)injectCSSScript
{
NSString *css = [DHCSS currentCSSStringWithTextModifier];
css = [[css stringByReplacingOccurrencesOfString:@"\n" withString:@"\\\n"] stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];
return [NSString stringWithFormat:@"var style = document.createElement('style'); style.innerText = '%@'; document.head.insertBefore(style, document.head.childNodes[0]);", css];
}
- (NSString *)injectViewPortScript
{
return [NSString stringWithFormat:@"var surogate = document.createElement('div'); surogate.innerHTML = \"\"; var meta = surogate.childNodes[0]; document.head.appendChild(meta);", [DHWebView viewportContent:[DHWebViewController sharedWebViewController].webView.frame]];
}
@end
================================================
FILE: Dash/DHJavaScriptBridge.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import
@protocol DHJavaScriptBridgeExport
- (void)switchAppleLanguage:(JSValue *)value;
- (void)newSwitchAppleLanguage:(JSValue *)nameValue;
- (void)log:(JSValue *)value;
- (void)coffeeScriptOpenLink_:(NSString *)string;
- (void)unityConsoleLog:(NSString *)message;
- (void)showFallbackExplanation;
- (void)loadFallbackURL_:(JSValue *)suppressButtonChecked;
- (void)openDownloads;
- (void)openDocsets;
- (void)openProfiles;
- (void)openGuide;
- (void)openIOSLink;
- (void)webViewDidChangeLocationWithinPage;
@end
@interface DHJavaScriptBridge : NSObject
@property (nonatomic, copy) void (^alertBlock)(JSValue *message);
+ (DHJavaScriptBridge *)sharedBridge;
@end
================================================
FILE: Dash/DHJavaScriptBridge.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHJavaScriptBridge.h"
#import "DHWebViewController.h"
#import "DHCSS.h"
@implementation DHJavaScriptBridge
+ (DHJavaScriptBridge *)sharedBridge
{
static dispatch_once_t pred;
static DHJavaScriptBridge *_singleton = nil;
dispatch_once(&pred, ^{
_singleton = [[DHJavaScriptBridge alloc] init];
_singleton.alertBlock = ^(JSValue *message) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"JavaScript Alert" message:[message toString] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
};
});
return _singleton;
}
- (void)switchAppleLanguage:(JSValue *)nameValue
{
NSString *name = [nameValue toString];
name = [name trimWhitespace];
if(name && name.length)
{
[[NSUserDefaults standardUserDefaults] setObject:name forKey:DHActiveAppleLanguageKey];
[[DHCSS sharedCSS] refreshActiveCSS];
[[DHWebViewController sharedWebViewController] reload];
if([DHRemoteServer sharedServer].connectedRemote)
{
[[DHRemoteServer sharedServer] sendObject:@{@"language": name} forRequestName:@"syncAppleLanguage" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
}
}
- (void)newSwitchAppleLanguage:(JSValue *)nameValue
{
NSString *text = [[nameValue toString] trimWhitespace];
if([text contains:@"Swift"])
{
[DHAppleActiveLanguage setLanguage:DHNewActiveAppleLanguageSwift];
}
else if([text contains:@"Objective-C"] || [text contains:@"ObjC"])
{
[DHAppleActiveLanguage setLanguage:DHNewActiveAppleLanguageObjC];
}
if([DHRemoteServer sharedServer].connectedRemote)
{
[[DHRemoteServer sharedServer] sendObject:@{@"language": @([DHAppleActiveLanguage currentLanguage])} forRequestName:@"syncNewAppleLanguage" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
}
- (void)unityConsoleLog:(NSString *)message
{
if([message isKindOfClass:[JSValue class]])
{
message = [(JSValue*)message toString];
}
if([message isKindOfClass:[NSString class]] && [message isEqualToString:@"selected"])
{
NSString *value = [[[DHWebViewController sharedWebViewController] webView] stringByEvaluatingJavaScriptFromString:@"document.getElementsByClassName('cSelect-Selected')[0].innerText"];
if(value && value.length)
{
[[NSUserDefaults standardUserDefaults] setObject:value forKey:@"unitySelectedSnippetLanguage"];
}
}
}
- (void)coffeeScriptOpenLink_:(NSString *)string
{
NSURL *url = [NSURL URLWithString:[@"http://coffeescript.org/" stringByAppendingString:string]];
if(url)
{
[[UIApplication sharedApplication] openURL:url];
}
}
- (void)showFallbackExplanation
{
[[DHRemoteServer sharedServer] sendObject:@{@"selector": @"showFallbackExplanation", @"shouldShowWindow": @YES} forRequestName:@"performWebSelector" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
- (void)loadFallbackURL_:(JSValue *)suppressButtonChecked
{
[[DHRemoteServer sharedServer] sendObject:@{@"selector": @"loadFallbackURL:", @"arg": @([suppressButtonChecked toBool])} forRequestName:@"performWebSelector" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
- (void)openDownloads
{
[[DHRemoteServer sharedServer] sendObject:@{@"selector": @"openDownloads"} forRequestName:@"performWebSelector" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
- (void)openDocsets
{
[[DHRemoteServer sharedServer] sendObject:@{@"selector": @"openDocsets"} forRequestName:@"performWebSelector" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
- (void)openProfiles
{
[[DHRemoteServer sharedServer] sendObject:@{@"selector": @"openProfiles", @"shouldShowWindow": @YES} forRequestName:@"performWebSelector" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
- (void)openGuide
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://kapeli.com/dash_guide"]];
}
- (void)openIOSLink
{
[UIAlertView showWithTitle:@"Dash for iOS" message:@"You're using it!" cancelButtonTitle:@"Okay" otherButtonTitles:nil tapBlock:nil];
}
- (void)webViewDidChangeLocationWithinPage
{
[[DHWebViewController sharedWebViewController] webViewDidChangeLocationWithinPage];
}
- (void)log:(JSValue *)value
{
NSLog(@"JS Log: %@", value);
}
@end
================================================
FILE: Dash/DHLatencyTestResult.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHLatencyTestResult : NSObject
@property (retain) NSString *host;
@property (assign) double latency;
@property (retain) NSDate *startTestDate;
@property (retain) NSDate *lastTestDate;
@property (assign) BOOL isPerformingTest;
+ (DHLatencyTestResult *)resultWithDictionaryRepresentation:(NSDictionary *)dictionary;
+ (DHLatencyTestResult *)resultWithHost:(NSString *)host latency:(double)latency;
- (NSDictionary *)dictionaryRepresentation;
- (void)performTest;
- (BOOL)shouldPerformTest;
- (double)adaptiveLatency;
@end
================================================
FILE: Dash/DHLatencyTestResult.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHLatencyTestResult.h"
#import "DHLatencyTester.h"
@implementation DHLatencyTestResult
+ (DHLatencyTestResult *)resultWithDictionaryRepresentation:(NSDictionary *)dictionary
{
return [DHLatencyTestResult resultWithHost:dictionary[@"host"] latency:[dictionary[@"latency"] doubleValue]];
}
+ (DHLatencyTestResult *)resultWithHost:(NSString *)host latency:(double)latency
{
DHLatencyTestResult *result = [[DHLatencyTestResult alloc] init];
result.host = host;
result.latency = latency;
return result;
}
- (NSDictionary *)dictionaryRepresentation
{
return @{@"host": self.host, @"latency": @(self.latency)};
}
- (BOOL)shouldPerformTest
{
return (!self.lastTestDate || [[NSDate date] timeIntervalSinceDate:self.lastTestDate] > 60) && !self.isPerformingTest;
}
- (void)performTest
{
if([self shouldPerformTest])
{
self.startTestDate = [NSDate date];
self.isPerformingTest = YES;
double minLatency = 10000.0;
BOOL success = NO;
BOOL didCheckExtraMirrors = NO;
for(int i = 0; i < 3; i++)
{
success = NO;
self.startTestDate = [NSDate date];
NSDate *then = [NSDate date];
NSString *theHost = self.host;
if([theHost hasSuffix:@"/"])
{
theHost = [theHost substringToDashIndex:theHost.length-1];
}
NSURL *url = [NSURL URLWithString:[[theHost stringByAppendingFormat:@"/latencyTest_v2.txt?cache_buster=%u", arc4random() % 1000000] stringByConvertingKapeliHttpURLToHttps]];
if(url)
{
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0f];
NSURLResponse *response = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];
NSString *string = nil;
if(data)
{
string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if(string && [string hasPrefix:@"Just a latency test. Move along."])
{
NSTimeInterval latency = [[NSDate date] timeIntervalSinceDate:then];
if(minLatency > latency)
{
minLatency = latency;
self.latency = latency;
}
success = YES;
if(!didCheckExtraMirrors)
{
// E.g. Extra mirrors: http://newyork3.kapeli.com/feeds/, http://newyork4.kapeli.com/feeds/
NSString *mirrorsString = [string substringFromStringReturningNil:@"Extra mirrors: "];
if(mirrorsString && mirrorsString.length)
{
NSMutableArray *mirrors = [NSMutableArray arrayWithArray:[mirrorsString componentsSeparatedByString:@", "]];
if(mirrors && mirrors.count)
{
didCheckExtraMirrors = YES;
dispatch_async(dispatch_get_main_queue(), ^{
[[DHLatencyTester sharedLatency] checkExtraMirrors:mirrors];
});
}
}
}
}
}
if(!success)
{
self.latency = 10000.0;
break;
}
}
}
if(success && minLatency > 0 && minLatency < 10000.0)
{
self.latency = minLatency;
}
// NSLog(@"%f for %@", self.latency, self.host);
self.lastTestDate = [NSDate date];
self.startTestDate = nil;
self.isPerformingTest = NO;
}
}
- (double)adaptiveLatency
{
if(self.isPerformingTest && self.startTestDate)
{
double interval = [[NSDate date] timeIntervalSinceDate:self.startTestDate];
return (interval > self.latency) ? interval : self.latency;
}
return self.latency;
}
- (BOOL)isEqual:(id)object
{
return [self.host isEqualToString:[object host]];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@ - %f", self.host, [self adaptiveLatency]];
}
@end
================================================
FILE: Dash/DHLatencyTester.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHLatencyTester : NSObject
@property (retain) NSMutableArray *results;
@property (retain) NSOperationQueue *queue;
@property (retain) NSMutableArray *defaultResults;
@property (retain) NSMutableArray *resultsAllowedInUserDefaults;
+ (DHLatencyTester *)sharedLatency;
- (BOOL)performTests:(BOOL)forcePerform;
- (void)saveDefaults;
- (void)sortURLsBasedOnLatency:(NSMutableArray *)urls;
- (void)checkExtraMirrors:(NSMutableArray *)mirrors;
- (NSString *)bestMirrorReturningNil;
- (NSString *)bestMirror;
- (NSString *)secondBestMirror;
- (NSMutableArray *)sortedTestResults;
@end
================================================
FILE: Dash/DHLatencyTester.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHLatencyTester.h"
#import "DHLatencyTestResult.h"
@implementation DHLatencyTester
+ (DHLatencyTester *)sharedLatency
{
static dispatch_once_t pred;
static DHLatencyTester *_latency = nil;
dispatch_once(&pred, ^{
_latency = [[DHLatencyTester alloc] init];
[_latency setUp];
});
return _latency;
}
- (BOOL)performTests:(BOOL)forcePerform
{
BOOL didPerform = NO;
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"didPerformLatencyTestsBefore"];
@synchronized(self)
{
if(!self.queue)
{
self.queue = [[NSOperationQueue alloc] init] ;
[self.queue setMaxConcurrentOperationCount:1];
}
for(DHLatencyTestResult *result in self.results)
{
if(forcePerform)
{
result.lastTestDate = nil;
}
if([result shouldPerformTest])
{
didPerform = YES;
[self.queue addOperationWithBlock:^{
__block BOOL done = NO;
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[result performTest];
done = YES;
});
NSDate *startDate = [NSDate date];
while(!done && [[NSDate date] timeIntervalSinceDate:startDate] < 15 && (!result.startTestDate || [[NSDate date] timeIntervalSinceDate:result.startTestDate] < 3))
{
[NSThread sleepForTimeInterval:0.03];
}
}];
}
}
if(didPerform)
{
[self.queue addOperationWithBlock:^{
dispatch_sync(dispatch_get_main_queue(), ^{
[self saveDefaults];
});
}];
}
}
return didPerform;
}
- (void)setUp
{
self.resultsAllowedInUserDefaults = [NSMutableArray array];
self.defaultResults = [NSMutableArray array];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL didPerformTestsBefore = [defaults boolForKey:@"didPerformLatencyTestsBefore"];
self.results = [NSMutableArray array];
NSMutableArray *resultsTemp = [NSMutableArray array];
for(NSDictionary *resultDict in [defaults objectForKey:@"latencyTestResults"])
{
[self.defaultResults addObject:[DHLatencyTestResult resultWithDictionaryRepresentation:resultDict]];
[resultsTemp addObject:self.defaultResults.lastObject];
}
for(NSString *host in @[@"http://sanfrancisco.kapeli.com/feeds/", @"http://newyork.kapeli.com/feeds/", @"http://london.kapeli.com/feeds/"])
{
DHLatencyTestResult *result = [DHLatencyTestResult resultWithHost:host latency:10000.0];
[self.resultsAllowedInUserDefaults addObject:result];
if(![resultsTemp containsObject:result])
{
[resultsTemp addObject:result];
}
}
[self sortLatencyTestResults:resultsTemp];
self.results = [NSMutableArray arrayWithArray:resultsTemp];
if(didPerformTestsBefore)
{
[self performTests:NO];
}
}
- (void)saveDefaults
{
if(![NSThread isMainThread])
{
[self performSelectorOnMainThread:@selector(saveDefaults) withObject:nil waitUntilDone:YES];
return;
}
NSMutableArray *array = [NSMutableArray array];
@synchronized(self)
{
for(DHLatencyTestResult *result in self.results)
{
if(result.latency < 60 && [self.resultsAllowedInUserDefaults containsObject:result])
{
[array addObject:[result dictionaryRepresentation]];
}
}
}
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:array forKey:@"latencyTestResults"];
}
- (NSString *)bestMirrorReturningNil
{
if(!self.results.count)
{
return nil;
}
return [[self sortedTestResults][0] host];
}
- (NSString *)secondBestMirrorReturningNil
{
if(self.results.count < 2)
{
return nil;
}
return [[self sortedTestResults][1] host];
}
- (NSMutableArray *)sortedTestResults
{
NSMutableArray *results = [NSMutableArray arrayWithArray:self.results];
[self sortLatencyTestResults:results];
return results;
}
- (void)sortLatencyTestResults:(NSMutableArray *)results
{
[results sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
double myLatency = [obj1 adaptiveLatency];
double theirLatency = [obj2 adaptiveLatency];
if(myLatency < theirLatency)
{
return NSOrderedAscending;
}
else if(myLatency > theirLatency)
{
return NSOrderedDescending;
}
return NSOrderedSame;
}];
}
- (NSString *)bestMirror
{
NSString *best = [self bestMirrorReturningNil];
if(!best)
{
return @"http://kapeli.com/feeds/";
}
return best;
}
- (NSString *)secondBestMirror
{
NSString *best = [self secondBestMirrorReturningNil];
if(!best)
{
return @"http://london.kapeli.com/feeds/";
}
return best;
}
- (void)sortURLsBasedOnLatency:(NSMutableArray *)urls
{
[urls sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
double myLatency = [self latencyForURL:obj1];
double theirLatency = [self latencyForURL:obj2];
if(myLatency < theirLatency)
{
return NSOrderedAscending;
}
else if(myLatency > theirLatency)
{
return NSOrderedDescending;
}
return NSOrderedSame;
}];
}
- (double)latencyForURL:(NSString *)aURL
{
@synchronized(self)
{
for(DHLatencyTestResult *result in self.results)
{
if([aURL hasCaseInsensitivePrefix:result.host])
{
return [result adaptiveLatency];
}
}
}
return 10000.0;
}
- (void)checkExtraMirrors:(NSMutableArray *)mirrors
{
@synchronized(self)
{
if(mirrors.count)
{
for(NSString *mirror in mirrors)
{
DHLatencyTestResult *result = [DHLatencyTestResult resultWithHost:mirror latency:10000.0];
if(![self.results containsObject:result])
{
[self.results addObject:result];
}
if(![self.resultsAllowedInUserDefaults containsObject:result])
{
[self.resultsAllowedInUserDefaults addObject:result];
}
}
[self performTests:NO];
}
}
}
@end
================================================
FILE: Dash/DHLoadingCell.xib
================================================
================================================
FILE: Dash/DHNavigationAnimator.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHNavigationAnimator : NSObject
@property (assign) BOOL noAnimation;
@end
================================================
FILE: Dash/DHNavigationAnimator.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHNavigationAnimator.h"
#import "DHPreferences.h"
#import "DHWebViewController.h"
@implementation DHNavigationAnimator
- (NSTimeInterval)transitionDuration:(id )transitionContext
{
if(self.noAnimation)
{
return 0.00001;
}
return 0.3;
}
- (void)animateTransition:(id)transitionContext
{
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
if(!isRegularHorizontalClass)
{
BOOL isOpening = [toViewController isKindOfClass:[DHPreferences class]];
if(isOpening)
{
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 1;
CGRect endFrame = fromViewController.view.frame;
CGRect startFrame = endFrame;
startFrame.origin.y = CGRectGetMaxY(endFrame);
toViewController.view.frame = startFrame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.frame = endFrame;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:YES];
}];
}
else
{
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
toViewController.view.alpha = 1;
CGRect endFrame = fromViewController.view.frame;
toViewController.view.frame = fromViewController.view.frame;
endFrame.origin.y = CGRectGetMaxY(endFrame);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.frame = endFrame;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:YES];
}];
}
}
else
{
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0;
toViewController.view.frame = fromViewController.view.frame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.alpha = 1;
} completion:^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:YES];
}];
}
}
@end
================================================
FILE: Dash/DHNavigationController.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHNavigationController : UINavigationController
@end
================================================
FILE: Dash/DHNavigationController.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHNavigationController.h"
@implementation DHNavigationController
@end
================================================
FILE: Dash/DHNestedViewController.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHDBResult.h"
#import "DHWebViewController.h"
@interface DHNestedViewController : UITableViewController
@property (retain) DHDBResult *result;
@property (assign) BOOL hasMultipleDocsets;
@property (assign) BOOL didDecode;
@end
================================================
FILE: Dash/DHNestedViewController.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHNestedViewController.h"
#import "DHBrowserTableViewCell.h"
@implementation DHNestedViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.clearsSelectionOnViewWillAppear = NO;
self.title = self.result.name;
[self.tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
for(DHDBResult *result in self.result.similarResults)
{
if(result.docset != self.result.docset)
{
self.hasMultipleDocsets = YES;
}
else if(result.isRemote && ![result.remoteDocsetName isEqualToString:self.result.remoteDocsetName])
{
self.hasMultipleDocsets = YES;
}
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(isRegularHorizontalClass)
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:[self activeRow] inSection:0] animated:NO scrollPosition:UITableViewScrollPositionNone];
});
}
else
{
if(self.result.isRemote)
{
if(self.tableView.indexPathForSelectedRow)
{
[self.tableView scrollToRowAtIndexPath:self.tableView.indexPathForSelectedRow atScrollPosition:UITableViewScrollPositionNone animated:NO];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.tableView deselectAll:YES];
});
}
else
{
[self.tableView deselectAll:YES];
}
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if(isRegularHorizontalClass && !self.didDecode)
{
[[DHDBResultSorter sharedSorter] resultWasSelected:[self resultForRow:[self activeRow]] inTableView:self.tableView];
}
self.didDecode = NO;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
[self.tableView reloadData];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.result.similarResults.count+1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DHBrowserTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHBrowserCell" forIndexPath:indexPath];
DHDBResult *result = [self resultForRow:indexPath.row];
[cell makeEntryCell];
cell.textLabel.attributedText = nil;
NSString *title = [result.declaredInPage substringFromString:@" - "];
title = (title.length) ? title : result.originalName;
cell.textLabel.text = title;
if(self.hasMultipleDocsets)
{
cell.titleLabel.subtitle = (result.isRemote) ? result.remoteDocsetName : result.docset.name;
}
cell.titleLabel.font = [UIFont fontWithName:@"Menlo" size:16];
cell.typeImageView.image = result.typeImage;
cell.platformImageView.image = result.platformImage;
cell.accessoryType = (!isRegularHorizontalClass) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
return cell;
}
- (void)clearIsActive
{
self.result.isActive = NO;
for(DHDBResult *result in self.result.similarResults)
{
result.isActive = NO;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
DHDBResult *result = [self resultForRow:tableView.indexPathForSelectedRow.row];
[[DHDBResultSorter sharedSorter] resultWasSelected:result inTableView:tableView];
[[DHDBNestedResultSorter sharedSorter] increaseRankForResult:result];
if(isRegularHorizontalClass)
{
[self clearIsActive];
result.isActive = YES;
[[DHWebViewController sharedWebViewController] loadResult:result];
}
else
{
[[DHWebViewController sharedWebViewController] loadResult:result];
[self performSegueWithIdentifier:@"DHSearchWebViewSegue" sender:self];
}
if(result.isRemote)
{
NSString *remoteName = [DHRemoteServer sharedServer].connectedRemote.name;
if(remoteName)
{
[[DHRemoteServer sharedServer] sendObject:@{@"selectedNestedRow": @(tableView.indexPathForSelectedRow.row), @"selectedRowName": (self.result.name) ? : @"justsendtheresults"} forRequestName:@"syncNestedSelectedRow" encrypted:YES toMacName:remoteName];
}
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:@"DHSearchWebViewSegue"])
{
DHWebViewController *webViewController = [segue destinationViewController];
DHDBResult *result = [self resultForRow:self.tableView.indexPathForSelectedRow.row];
webViewController.result = result;
}
}
- (DHDBResult *)resultForRow:(NSInteger)row
{
return row == 0 ? self.result : self.result.similarResults[row-1];
}
- (NSInteger)activeRow
{
int i = 1;
for(DHDBResult *result in self.result.similarResults)
{
if(result.isActive)
{
return i;
}
++i;
}
return 0;
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:self.result forKey:@"result"];
[coder encodeBool:self.hasMultipleDocsets forKey:@"hasMultipleDocsets"];
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if(selectedIndexPath != nil)
{
[coder encodeObject:selectedIndexPath forKey:@"selectedIndexPath"];
}
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder];
self.didDecode = YES;
self.result = [coder decodeObjectForKey:@"result"];
self.hasMultipleDocsets = [coder decodeBoolForKey:@"hasMultipleDocsets"];
self.title = self.result.name;
NSIndexPath *selectedIndexPath = [coder decodeObjectForKey:@"selectedIndexPath"];
if(selectedIndexPath != nil)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
});
}
}
@end
================================================
FILE: Dash/DHPreferences.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHPreferences : UITableViewController
@property (assign) IBOutlet UISwitch *updatesSwitch;
@property (assign) IBOutlet UISwitch *alphabetizingSwitch;
@property (assign) IBOutlet UITableViewCell *updatesCell;
@property (assign) IBOutlet UITableViewCell *alphabetizingCell;
@property (assign) BOOL didSetUpdateLabelBefore;
- (IBAction)updatesSwitchValueChanged:(id)sender;
- (IBAction)getDashForMacOS:(id)sender;
- (NSString *)segueIdentifierForIndexPath:(NSIndexPath *)indexPath;
@end
================================================
FILE: Dash/DHPreferences.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHPreferences.h"
#import "DHWebViewController.h"
#import "DHAppDelegate.h"
#import "DHDocsetDownloader.h"
#import "DHDocsetTransferrer.h"
@implementation DHPreferences
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareForURLSearch:) name:DHPrepareForURLSearch object:nil];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.updatesSwitch setOn:[[NSUserDefaults standardUserDefaults] boolForKey:[[DHDocsetDownloader sharedDownloader] defaultsAutomaticallyCheckForUpdatesKey]]];
[self.alphabetizingSwitch setOn:[NSUserDefaults.standardUserDefaults boolForKey:DHDocsetDownloader.defaultsAlphabetizingKey]];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self updateAlphabetizingSwitchFooterView:nil];
[self updateUpdatesSwitchFooterView:nil];
});
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.navigationController setToolbarHidden:NO animated:YES];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.navigationController.toolbar setHidden:YES];
[self.navigationController setToolbarHidden:YES animated:NO];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
[self.tableView reloadData];
if(isRegularHorizontalClass)
{
self.clearsSelectionOnViewWillAppear = NO;
if(!self.tableView.indexPathForSelectedRow)
{
UIViewController *controller = [[self.splitViewController.viewControllers lastObject] topViewController];
NSString *controllerTitle = controller.navigationItem.title;
BOOL found = NO;
for(NSInteger section = 0; section < self.tableView.numberOfSections; section++)
{
for(NSInteger row = 0; row < [self.tableView numberOfRowsInSection:section]; row++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if([cell.textLabel.text isEqualToString:controllerTitle])
{
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
found = YES;
break;
}
}
}
if(!found)
{
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
}
else
{
[self.tableView deselectAll:YES];
self.clearsSelectionOnViewWillAppear = YES;
}
}
- (IBAction)dismissModal:(id)sender
{
[self.navigationController popViewControllerAnimated:YES];
UINavigationController *rightNavController = self.splitViewController.viewControllers.lastObject;
id toPopTo = nil;
for(id viewController in [[rightNavController.viewControllers reverseObjectEnumerator] allObjects])
{
if([viewController isKindOfClass:[DHWebViewController class]])
{
toPopTo = viewController;
break;
}
}
if(toPopTo)
{
[(DHWebViewController *)toPopTo view].frame = rightNavController.view.bounds;
[rightNavController popToViewController:toPopTo animated:YES];
}
else
{
[rightNavController popViewControllerAnimated:YES];
}
[[rightNavController navigationItem] setHidesBackButton:NO];
}
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
if([[self.tableView cellForRowAtIndexPath:indexPath] selectionStyle] == UITableViewCellSelectionStyleNone)
{
return NO;
}
return YES;
}
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// prevent selection of rows that are already selected
if(isRegularHorizontalClass && [tableView.indexPathForSelectedRow isEqual:indexPath])
{
return nil;
}
if([[self.tableView cellForRowAtIndexPath:indexPath] selectionStyle] == UITableViewCellSelectionStyleNone)
{
return nil;
}
return indexPath;
}
- (NSString *)segueIdentifierForIndexPath:(NSIndexPath *)indexPath
{
NSString *title = [[[self.tableView cellForRowAtIndexPath:indexPath] textLabel] text];
if([title isEqualToString:@"Main Docsets"])
{
if(isRegularHorizontalClass)
{
// Also used by DHSplitViewController. Make sure identifiers end with "ToDetailSegue"
return @"DHDocsetDownloaderToDetailSegue";
}
return @"DHDocsetDownloaderToMasterSegue";
}
else if([title isEqualToString:@"User Contributed Docsets"])
{
if(isRegularHorizontalClass)
{
// Also used by DHSplitViewController. Make sure identifiers end with "ToDetailSegue"
return @"DHUserRepoToDetailSegue";
}
return @"DHUserRepoToMasterSegue";
}
else if([title isEqualToString:@"Cheat Sheets"])
{
if(isRegularHorizontalClass)
{
// Also used by DHSplitViewController. Make sure identifiers end with "ToDetailSegue"
return @"DHCheatRepoToDetailSegue";
}
return @"DHCheatRepoToMasterSegue";
}
else if([title isEqualToString:@"Transfer Docsets"])
{
if(isRegularHorizontalClass)
{
// Also used by DHSplitViewController. Make sure identifiers end with "ToDetailSegue"
return @"DHDocsetTransferrerToDetailSegue";
}
return @"DHDocsetTransferrerToMasterSegue";
}
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *segueIdentifier = [self segueIdentifierForIndexPath:indexPath];
if(segueIdentifier)
{
[self performSegueWithIdentifier:[self segueIdentifierForIndexPath:indexPath] sender:self];
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if(isRegularHorizontalClass)
{
UINavigationController *rightNavController = self.splitViewController.viewControllers.lastObject;
NSMutableArray *newViewControllers = [NSMutableArray array];
for(NSUInteger i = 0; i < rightNavController.viewControllers.count; i++)
{
id vc = rightNavController.viewControllers[i];
if([vc isKindOfClass:[DHWebViewController class]] || i == rightNavController.viewControllers.count-1)
{
[newViewControllers addObject:vc];
}
}
[rightNavController setViewControllers:newViewControllers];
}
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(isRegularHorizontalClass)
{
UIView *bgColorView = [[UIView alloc] init];
bgColorView.backgroundColor = [DHAppDelegate sharedDelegate].window.rootViewController.view.tintColor;
bgColorView.layer.masksToBounds = YES;
cell.textLabel.highlightedTextColor = [UIColor whiteColor];
cell.detailTextLabel.highlightedTextColor = [UIColor whiteColor];
cell.selectedBackgroundView = bgColorView;
cell.accessoryType = UITableViewCellAccessoryNone;
}
else
{
if(cell.selectionStyle != UITableViewCellSelectionStyleNone)
{
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.textLabel.highlightedTextColor = [UIColor blackColor];
cell.detailTextLabel.highlightedTextColor = [UIColor blackColor];
cell.selectedBackgroundView = nil;
}
}
- (IBAction)updatesSwitchValueChanged:(id)sender
{
[[NSUserDefaults standardUserDefaults] setBool:[sender isOn] forKey:[DHDocsetDownloader sharedDownloader].defaultsAutomaticallyCheckForUpdatesKey];
[self updateUpdatesSwitchFooterView:nil];
}
- (IBAction)alphabetizingSwitchValueChanged:(id)sender
{
[[NSUserDefaults standardUserDefaults] setBool:[sender isOn] forKey:DHDocsetDownloader.defaultsAlphabetizingKey];
[NSNotificationCenter.defaultCenter postNotificationName:DHSettingsChangedNotification object:DHDocsetDownloader.defaultsAlphabetizingKey];
[self updateAlphabetizingSwitchFooterView:nil];
}
- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UITableViewHeaderFooterView *)footer forSection:(NSInteger)section
{
if(section == [tableView indexPathForCell:self.updatesCell].section)
{
[self updateUpdatesSwitchFooterView:footer];
}
else if(section == [tableView indexPathForCell:self.alphabetizingCell].section)
{
[self updateAlphabetizingSwitchFooterView:footer];
}
}
- (void)updateUpdatesSwitchFooterView:(UITableViewHeaderFooterView *)footer
{
footer = (footer) ? footer : [self.tableView footerViewForSection:[self.tableView indexPathForCell:self.updatesCell].section];
[footer textLabel].text = [NSString stringWithFormat:@"Dash %@ notify you when docset updates are available.", (self.updatesSwitch.isOn) ? @"will" : @"won't"];
[[footer textLabel] sizeToFit];
}
- (void)updateAlphabetizingSwitchFooterView:(UITableViewHeaderFooterView *)footer
{
footer = (footer) ? footer : [self.tableView footerViewForSection:[self.tableView indexPathForCell:self.alphabetizingCell].section];
[footer textLabel].text = [NSString stringWithFormat:@"Docsets %@ be sorted alphabetically in the docset browser.", (self.alphabetizingSwitch.isOn) ? @"will" : @"won't"];
[[footer textLabel] sizeToFit];
}
- (void)prepareForURLSearch:(id)sender
{
[self dismissModal:self];
}
- (UIModalTransitionStyle)modalTransitionStyle
{
return [super modalTransitionStyle];
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if(selectedIndexPath != nil)
{
[coder encodeObject:selectedIndexPath forKey:@"selectedIndexPath"];
}
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder];
NSIndexPath *selectedIndexPath = [coder decodeObjectForKey:@"selectedIndexPath"];
if(selectedIndexPath != nil)
{
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
else
selectedIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
if (self.splitViewController.viewControllers.count == 2 && [selectedIndexPath compare:[NSIndexPath indexPathForRow:0 inSection:0]] == NSOrderedSame) {
UINavigationController *nav = [self.splitViewController.viewControllers lastObject];
if ([nav isKindOfClass:[UINavigationController class]] && ![nav.topViewController isKindOfClass:[DHDocsetDownloader class]]) {
NSMutableArray *newViewControllers = [NSMutableArray array];
[newViewControllers addObjectsFromArray:nav.viewControllers];
[newViewControllers addObject:[DHDocsetDownloader sharedDownloader]];
[nav setViewControllers:newViewControllers];
}
}
}
- (IBAction)getDashForMacOS:(id)sender
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://kapeli.com/dash"]];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
================================================
FILE: Dash/DHQueuedDB.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
#import "FMResultSet.h"
#import "DHDBResult.h"
@interface DHQueuedDB : NSObject
@property (retain, nonatomic) NSString *query;
@property (retain, nonatomic) NSMutableArray *queryQueue;
@property (retain, nonatomic) FMResultSet *currentRS;
@property (retain, nonatomic) NSString *dbPath;
@property (retain, nonatomic) FMDatabase *db;
@property (retain, nonatomic) DHDocset *docset;
@property (assign, nonatomic) BOOL hadPerfect;
@property (retain, nonatomic) NSMutableDictionary *resultDictionary;
@property (retain, nonatomic) NSConditionLock *lock;
+ (DHQueuedDB *)queueWithDocset:(DHDocset *)docset query:(NSString *)query typeLimit:(NSString *)typeLimit isFuzzy:(BOOL)isFuzzy;
- (BOOL)next;
- (BOOL)step;
- (DHDBResult *)currentDBResult;
- (void)addResultToResultDictionary:(DHDBResult *)result;
- (void)sortResultDictionary;
- (NSMutableArray *)resultsForType:(NSString *)type;
@end
================================================
FILE: Dash/DHQueuedDB.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHQueuedDB.h"
#import "DHDBSearcher.h"
@implementation DHQueuedDB
+ (DHQueuedDB *)queueWithDocset:(DHDocset *)docset query:(NSString *)query typeLimit:(NSString *)typeLimit isFuzzy:(BOOL)isFuzzy
{
DHQueuedDB *queue = [[DHQueuedDB alloc] init];
queue.resultDictionary = [NSMutableDictionary dictionary];
queue.query = query;
queue.queryQueue = [NSMutableArray array];
queue.dbPath = docset.optimisedIndexPath;
queue.docset = docset;
queue.db = [FMDatabase databaseWithPath:queue.dbPath];
queue.lock = [DHDocset stepLock];
[queue.lock lock];
BOOL didOpen = [queue.db openWithFlags:SQLITE_OPEN_READONLY];
[queue.db registerFTSExtensions];
[queue.db setLogsErrors:YES];
[queue.lock unlockWithCondition:DHLockSearchOnly];
if(!didOpen)
{
return nil;
}
if(!isFuzzy)
{
NSString *ftsEscapedQuery = [query stringByReplacingSpecialFTSCharacters];
NSString *prefixedQuery = [ftsEscapedQuery stringByAppendingString:@"*"];
NSString *likeQuery = [prefixedQuery stringByReplacingOccurrencesOfString:@"*" withString:@"%"];
[queue.queryQueue addObject:[DHQueuedDB queuedQueryDictionary:@"SELECT path, name, type FROM searchIndex s, queryIndex q WHERE q.rowid = s.rowid AND q.perfect MATCH ?" andArgs:@[ftsEscapedQuery]]];
[queue.queryQueue addObject:[DHQueuedDB queuedQueryDictionary:@"SELECT path, name, type FROM searchIndex s, queryIndex q WHERE q.rowid = s.rowid AND q.prefix MATCH ? LIMIT 200" andArgs:@[prefixedQuery]]];
[queue.queryQueue addObject:[DHQueuedDB queuedQueryDictionary:@"SELECT path, name, type FROM searchIndex s, queryIndex q WHERE q.rowid = s.rowid AND q.suffixes MATCH ? AND q.prefix NOT LIKE ? LIMIT 200" andArgs:@[ftsEscapedQuery, likeQuery]]];
[queue.queryQueue addObject:[DHQueuedDB queuedQueryDictionary:@"SELECT path, name, type FROM searchIndex s, queryIndex q WHERE q.rowid = s.rowid AND q.suffixes MATCH ? AND q.prefix NOT LIKE ? LIMIT 200" andArgs:@[[NSString stringWithFormat:@"%@ NOT %@", prefixedQuery, ftsEscapedQuery], likeQuery]]];
}
else if(query.length > 2)
{
NSString *escapedQuery = [query stringByReplacingOccurrencesOfString:@"~" withString:@"~~"];
escapedQuery = [escapedQuery stringByReplacingOccurrencesOfString:@"_" withString:@"~_"];
escapedQuery = [escapedQuery stringByReplacingOccurrencesOfString:@"%" withString:@"~%"];
NSString *wildcardEverywhere = [escapedQuery stringByAddingWildcardsEverywhere:@"~"];
[queue.queryQueue addObject:[DHQueuedDB queuedQueryDictionary:@"SELECT path, name, type FROM searchIndex WHERE name LIKE ? ESCAPE '~' AND name NOT LIKE ? ESCAPE '~' LIMIT 300;" andArgs:@[wildcardEverywhere, [[@"%" stringByAppendingString:escapedQuery] stringByAppendingString:@"%"]]]];
}
if(typeLimit)
{
for(NSMutableDictionary *dictionary in queue.queryQueue)
{
[dictionary[@"args"] insertObject:typeLimit atIndex:0];
dictionary[@"sqlQuery"] = [dictionary[@"sqlQuery"] stringByReplacingOccurrencesOfString:@" WHERE " withString:@" WHERE type = ? AND "];
}
}
return queue;
}
+ (NSDictionary *)queuedQueryDictionary:(NSString *)aQuery andArgs:(NSArray *)args
{
return [NSMutableDictionary dictionaryWithObjectsAndKeys:aQuery, @"sqlQuery", [args mutableCopy], @"args", nil];
}
- (BOOL)next
{
[DHDBSearcher checkForInterrupt];
[self.lock lock];
if([self.currentRS next])
{
[self.lock unlockWithCondition:DHLockSearchOnly];
[DHDBSearcher checkForInterrupt];
return YES;
}
[self.lock unlockWithCondition:DHLockSearchOnly];
[DHDBSearcher checkForInterrupt];
return NO;
}
- (BOOL)step
{
[DHDBSearcher checkForInterrupt];
if(self.queryQueue.count)
{
NSDictionary *aQuery = (self.queryQueue)[0];
[self.lock lock];
self.currentRS = [self resultSetFromQueuedQueryDictionary:aQuery];
[self.lock unlockWithCondition:DHLockSearchOnly];
[self.queryQueue removeObjectAtIndex:0];
[DHDBSearcher checkForInterrupt];
return YES;
}
[self close];
[DHDBSearcher checkForInterrupt];
return NO;
}
- (DHDBResult *)currentDBResult
{
DHDBResult *result = [DHDBResult resultWithDocset:self.docset resultSet:self.currentRS];
return [self prepareResult:result];
}
- (DHDBResult *)prepareResult:(DHDBResult *)result
{
[result setQuery:self.query];
NSString *name = [[result name] stringByReplacingOccurrencesOfString:@" " withString:@""];
NSString *originalName = [[result originalName] stringByReplacingOccurrencesOfString:@" " withString:@""];
if(![[result originalName] contains:self.query] && [originalName contains:self.query])
{
result.whitespaceMatch = YES;
}
self.hadPerfect = [originalName hasCaseInsensitiveSuffix:self.query] || [originalName hasCaseInsensitivePrefix:self.query];
[DHDBSearcher checkForInterrupt];
result.queryIsPrefix = [name hasCaseInsensitivePrefix:self.query];
result.queryIsSuffix = [name hasCaseInsensitiveSuffix:self.query];
result.perfectMatch = result.queryIsPrefix && result.queryIsSuffix && name.length == self.query.length;
if(self.query.length)
{
result.matchesQueryAtAll = (result.queryIsSuffix || result.queryIsPrefix || [name rangeOfString:self.query options:NSCaseInsensitiveSearch].location != NSNotFound);
result.originalMatchesQueryAtAll = result.matchesQueryAtAll || [originalName rangeOfString:self.query options:NSCaseInsensitiveSearch].location != NSNotFound;
result.queryIsPrefixOfOriginal = [originalName hasCaseInsensitivePrefix:self.query];
result.queryIsSuffixOfOriginal = [originalName hasCaseInsensitiveSuffix:self.query];
result.perfectMatchOriginal = result.queryIsPrefixOfOriginal && result.queryIsSuffixOfOriginal && originalName.length == self.query.length;
}
[result highlightWithQuery:self.query];
if(result.fuzzyShouldIgnore)
{
return nil;
}
return result;
}
- (FMResultSet *)resultSetFromQueuedQueryDictionary:(NSDictionary *)query
{
return [self.db executeQuery:query[@"sqlQuery"] withArgumentsInArray:query[@"args"]];
}
- (void)close
{
self.currentRS = nil;
if(self.db && self.db.sqliteHandle)
{
[self.lock lock];
[self.db close];
[self.lock unlockWithCondition:DHLockAllAllowed];
}
self.db = nil;
}
- (void)addResultToResultDictionary:(DHDBResult *)result
{
NSString *sortType = result.sortType;
NSMutableArray *typeResults = (self.resultDictionary)[sortType];
if(!typeResults)
{
typeResults = [NSMutableArray array];
(self.resultDictionary)[sortType] = typeResults;
}
[typeResults addObject:result];
}
- (void)sortResultDictionary
{
for(NSMutableArray *queueResults in [[self resultDictionary] allValues])
{
[queueResults sortUsingSelector:@selector(compareFuziness:)];
}
}
- (NSMutableArray *)resultsForType:(NSString *)type
{
return (self.resultDictionary)[type];
}
- (void)dealloc
{
[self close];
}
@end
================================================
FILE: Dash/DHRemote.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@class DHRemoteBrowser;
@interface DHRemote : NSObject
@property (strong) NSString *name;
@property (strong) UIImage *icon;
@property (weak) DHRemoteBrowser *browser;
+ (instancetype)remoteWithName:(NSString *)name icon:(UIImage *)icon;
- (void)connect;
- (void)disconnect;
@end
================================================
FILE: Dash/DHRemote.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRemote.h"
@implementation DHRemote
+ (instancetype)remoteWithName:(NSString *)name icon:(UIImage *)icon
{
DHRemote *remote = [[self alloc] init];
remote.name = name;
remote.icon = icon;
return remote;
}
- (BOOL)isEqual:(id)object
{
return [self.name isEqualToString:[object name]];
}
- (void)connect
{
[DHRemoteServer sharedServer].connectedRemote = self;
[[DHRemoteServer sharedServer] sendObject:nil forRequestName:@"connect" encrypted:YES toMacName:self.name];
}
- (void)disconnect
{
[DHRemoteServer sharedServer].requestsQueue = [NSMutableDictionary dictionary];
[[DHRemoteServer sharedServer] sendObject:nil forRequestName:@"disconnect" encrypted:YES toMacName:self.name];
[DHRemoteServer sharedServer].connectedRemote = nil;
}
@end
================================================
FILE: Dash/DHRemoteBrowser.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetBrowser.h"
@class DHNestedViewController;
@interface DHRemoteBrowser : UITableViewController
@property (strong) DHRemote *remote;
@property (strong) NSMutableArray *results;
@property (assign) BOOL isEmpty;
@property (strong) DHDBSearchController *searchController;
@property (assign) BOOL didFirstLoad;
@property (assign) BOOL isRestoring;
@property (assign) BOOL didLoad;
- (void)popNestedViewControllers;
- (DHNestedViewController *)nestedViewController;
@end
================================================
FILE: Dash/DHRemoteBrowser.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRemoteBrowser.h"
#import "DHNestedViewController.h"
#import "DHWebView.h"
@implementation DHRemoteBrowser
- (void)viewDidLoad
{
[super viewDidLoad];
self.clearsSelectionOnViewWillAppear = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareForURLSearch:) name:DHPrepareForURLSearch object:nil];
[self.tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
[self.tableView registerNib:[UINib nibWithNibName:@"DHLoadingCell" bundle:nil] forCellReuseIdentifier:@"DHLoadingCell"];
self.tableView.rowHeight = 44;
self.title = self.remote.name;
self.remote.browser = self;
[UIApplication sharedApplication].idleTimerDisabled = YES;
if(isRegularHorizontalClass)
{
DHWebViewController *webViewController = [DHWebViewController sharedWebViewController];
webViewController.webView.scrollView.delegate = nil;
webViewController.navigationController.toolbarHidden = YES;
[webViewController updateBackForwardButtonState];
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!isRegularHorizontalClass)
{
if(self.tableView.indexPathForSelectedRow)
{
[self.tableView scrollToRowAtIndexPath:self.tableView.indexPathForSelectedRow atScrollPosition:UITableViewScrollPositionNone animated:NO];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.tableView deselectAll:YES];
});
}
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
[self.tableView reloadData];
[self.searchController traitCollectionDidChange:previousTraitCollection];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:@"DHNestedSegue"])
{
DHNestedViewController *nestedController = [segue destinationViewController];
DHDBResult *result = self.results[self.tableView.indexPathForSelectedRow.row];
nestedController.result = result;
}
else if([[segue identifier] isEqualToString:@"DHSearchWebViewSegue"])
{
DHWebViewController *webViewController = [segue destinationViewController];
DHDBResult *result = self.results[self.tableView.indexPathForSelectedRow.row];
webViewController.result = result;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(tableView.indexPathForSelectedRow.row < self.results.count)
{
DHDBResult *result = self.results[tableView.indexPathForSelectedRow.row];
if(result.similarResults.count)
{
[self performSegueWithIdentifier:@"DHNestedSegue" sender:self];
}
else
{
if(!isRegularHorizontalClass)
{
[self performSegueWithIdentifier:@"DHSearchWebViewSegue" sender:self];
}
}
NSString *remoteName = [DHRemoteServer sharedServer].connectedRemote.name;
if(remoteName)
{
[[DHRemoteServer sharedServer] sendObject:@{@"selectedRow": @(tableView.indexPathForSelectedRow.row), @"selectedRowName": (result.name) ? : @"justsendtheresults"} forRequestName:@"syncSelectedRow" encrypted:YES toMacName:remoteName];
}
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
self.isEmpty = self.results.count <= 0;
if(self.isEmpty)
{
return 4;
}
return self.results.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(self.isEmpty)
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHLoadingCell" forIndexPath:indexPath];
cell.userInteractionEnabled = NO;
if(indexPath.row == 2)
{
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraph setAlignment:NSTextAlignmentCenter];
UIFont *font = [UIFont boldSystemFontOfSize:20];
cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Nothing here..." attributes:@{NSParagraphStyleAttributeName : paragraph, NSForegroundColorAttributeName: [UIColor colorWithWhite:0.8 alpha:1], NSFontAttributeName: font}];
}
else if(indexPath.row == 3)
{
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraph setAlignment:NSTextAlignmentCenter];
UIFont *font = [UIFont boldSystemFontOfSize:16];
cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:@"Search for something on your Mac" attributes:@{NSParagraphStyleAttributeName : paragraph, NSForegroundColorAttributeName: [UIColor colorWithWhite:0.8 alpha:1], NSFontAttributeName: font}];
}
else
{
cell.textLabel.text = @"";
}
return cell;
}
DHBrowserTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHBrowserCell" forIndexPath:indexPath];
DHDBResult *result = (indexPath.row) < self.results.count ? self.results[indexPath.row] : nil;
[cell makeEntryCell];
cell.textLabel.attributedText = nil;
cell.textLabel.font = [UIFont fontWithName:@"Menlo" size:16];
cell.textLabel.text = result.name;
cell.typeImageView.image = result.typeImage;
cell.platformImageView.image = result.platformImage;
[self highlightCell:cell result:result];
[cell.titleLabel setRightDetailText:(result.similarResults.count) ? [NSString stringWithFormat:@"%ld", (unsigned long)result.similarResults.count+1] : @"" adjustMainWidth:YES];
cell.accessoryType = (result.similarResults.count || !isRegularHorizontalClass) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
return cell;
}
- (void)highlightCell:(DHBrowserTableViewCell *)cell result:(DHDBResult *)result
{
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:cell.textLabel.attributedText];
for(NSString *key in [DHDBResult highlightDictionary])
{
[string removeAttribute:key range:NSMakeRange(0, string.length)];
}
BOOL didAddAttributes = NO;
for(NSValue *highlightRangeValue in result.highlightRanges)
{
NSRange highlightRange = [highlightRangeValue rangeValue];
[string addAttributes:[DHDBResult highlightDictionary] range:highlightRange];
didAddAttributes = YES;
}
if(didAddAttributes)
{
cell.textLabel.attributedText = string;
}
}
- (void)prepareForURLSearch:(id)sender
{
[self.searchDisplayController setActive:NO animated:NO];
}
- (void)popNestedViewControllers
{
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
for(UIViewController *viewController in self.navigationController.viewControllers)
{
if([viewController isKindOfClass:[DHNestedViewController class]])
{
[viewControllers removeObjectIdenticalTo:viewController];
}
}
[self.navigationController setViewControllers:viewControllers animated:YES];
}
- (DHNestedViewController *)nestedViewController
{
for(UIViewController *controller in self.navigationController.viewControllers)
{
if([controller isKindOfClass:[DHNestedViewController class]])
{
return (id)controller;
}
}
return nil;
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:self.remote forKey:@"remote"];
[coder encodeObject:self.results forKey:@"results"];
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if(selectedIndexPath != nil)
{
[coder encodeObject:selectedIndexPath forKey:@"selectedIndexPath"];
}
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder];
self.remote = [coder decodeObjectForKey:@"remote"];
[self.remote connect];
self.results = [coder decodeObjectForKey:@"results"];
NSIndexPath *selectedIndexPath = [coder decodeObjectForKey:@"selectedIndexPath"];
if(selectedIndexPath != nil)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView selectRowAtIndexPath:selectedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
});
}
}
- (void)dealloc
{
}
@end
================================================
FILE: Dash/DHRemoteImage.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHRemoteImage : NSObject
@end
================================================
FILE: Dash/DHRemoteImage.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRemoteImage.h"
@implementation DHRemoteImage
- (id)initWithCoder:(NSCoder *)decoder
{
UIImage *image = nil;
if([decoder containsValueForKey:@"name"])
{
image = [UIImage imageNamed:[decoder decodeObjectForKey:@"name"]];
}
else if([decoder containsValueForKey:@"data"])
{
image = [UIImage imageWithData:[decoder decodeObjectForKey:@"data"]];
}
image = (image) ? image : [UIImage imageNamed:@"Other"];
return (id)image;
}
@end
================================================
FILE: Dash/DHRemoteProtocol.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHRemoteProtocol : NSURLProtocol
@property (retain) NSString *path;
@property (retain) NSString *extension;
@property (retain) NSString *mimeType;
@property (retain) NSTimer *timeoutTimer;
@property (retain) NSString *identifier;
- (void)receivedData:(NSMutableData *)data userInfo:(NSDictionary *)userInfo isTimeout:(BOOL)isTimeout;
+ (NSDictionary *)lastResponseUserInfo;
@end
================================================
FILE: Dash/DHRemoteProtocol.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRemoteProtocol.h"
#import "DHRemoteServer.h"
#import "DHTarixProtocol.h"
@implementation DHRemoteProtocol
static NSDictionary *_lastResponseUserInfo;
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if([DHRemoteServer sharedServer].connectedRemote)
{
NSString *scheme = [[request URL] scheme];
if([scheme isCaseInsensitiveEqual:@"file"] || [scheme hasPrefix:@"dash-"]) // dash-remote-snippet, dash-stack, dash-tarix, dash-man-page, dash-apple-api
{
return YES;
}
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (void)startLoading
{
if(![[[self.request URL] scheme] isEqualToString:@"dash-remote-snippet"])
{
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
if(cachedResponse)
{
if(cachedResponse.userInfo)
{
_lastResponseUserInfo = cachedResponse.userInfo;
}
[[self client] URLProtocol:self didReceiveResponse:[cachedResponse response] cacheStoragePolicy:NSURLCacheStorageAllowed];
[[self client] URLProtocol:self didLoadData:[cachedResponse data]];
[[self client] URLProtocolDidFinishLoading:self];
return;
}
}
NSTimeInterval timeout = 60.0f;
#ifdef DEBUG
// timeout = 6.0f;
#endif
NSString *scheme = [[self.request URL] scheme];
self.path = [[[self.request URL] path] stringByReplacingPercentEscapes];
self.extension = [self.path pathExtension];
self.mimeType = [NSString mimeTypeForPathExtension:self.extension];
if([scheme hasPrefix:@"dash-"] && ![scheme isEqualToString:@"dash-tarix"])
{
self.mimeType = @"text/html";
}
self.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:@selector(requestDidTimeout) userInfo:nil repeats:NO];
DHRemoteServer *server = [DHRemoteServer sharedServer];
dispatch_sync(dispatch_get_main_queue(), ^{
while(YES)
{
self.identifier = [NSString randomStringWithLength:12];
if(!server.requestsQueue[self.identifier])
{
server.requestsQueue[self.identifier] = self;
break;
}
}
NSURL *url = self.request.URL;
if([[url scheme] isCaseInsensitiveEqual:@"dash-tarix"])
{
url = [NSURL URLWithString:[[url absoluteString] stringByReplacingOccurrencesOfString:@"dash-tarix://" withString:@"file://"]];
}
[server sendObject:@{@"url": url, @"identifier": self.identifier, @"mimeType": self.mimeType, @"extension": (self.extension) ? : @""} forRequestName:@"loadRequest" encrypted:YES toMacName:server.connectedRemote.name];
});
}
- (void)requestDidTimeout
{
[self stopLoading];
[self receivedData:nil userInfo:nil isTimeout:YES];
}
- (void)receivedData:(NSMutableData *)data userInfo:(NSDictionary *)userInfo isTimeout:(BOOL)isTimeout
{
if([userInfo[@"isInternal"] boolValue])
{
self.mimeType = @"text/html";
self.extension = @"html";
}
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[self.request URL] MIMEType:self.mimeType expectedContentLength:-1 textEncodingName:nil];
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
self.timeoutTimer = [self.timeoutTimer invalidateTimer];
BOOL hadData = data != nil;
data = [DHTarixProtocol alterData:data scheme:[[self.request URL] scheme] path:self.path extension:self.extension mimeType:self.mimeType isTimeout:isTimeout];
if(hadData && ([self.extension hasCaseInsensitivePrefix:@"htm"] || [self.mimeType contains:@"html"]))
{
_lastResponseUserInfo = userInfo;
}
if(!isTimeout && data)
{
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data userInfo:userInfo storagePolicy:NSURLCacheStorageAllowed];
[[NSURLCache sharedURLCache] storeCachedResponse:cachedResponse forRequest:self.request];
}
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
[self stopLoading];
}
+ (NSDictionary *)lastResponseUserInfo
{
return _lastResponseUserInfo;
}
- (void)stopLoading
{
self.timeoutTimer = [self.timeoutTimer invalidateTimer];
if(self.identifier)
{
@synchronized([DHRemoteProtocol class])
{
[[DHRemoteServer sharedServer].requestsQueue removeObjectForKey:self.identifier];
}
}
}
@end
================================================
FILE: Dash/DHRemoteServer.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DTBonjourServer.h"
#import "DHRemote.h"
@interface DHRemoteServer : NSObject
@property (strong) DTBonjourServer *server;
@property (strong) NSMutableDictionary *connections;
@property (strong) UIAlertView *shownAlert;
@property (assign) BOOL ignoreRequests;
@property (strong) NSMutableArray *remotes;
@property (strong) DHRemote *connectedRemote;
@property (strong) NSMutableDictionary *requestsQueue;
@property (retain) NSTimer *sendWebViewURLTimer;
@property (retain) NSArray *tableOfContentsMethods;
@property (assign) BOOL tableOfContentsIsSnippet;
@property (retain) NSMutableDictionary *lastDecryptFailDates;
+ (DHRemoteServer *)sharedServer;
- (void)sendObject:(id)object forRequestName:(NSString *)name encrypted:(BOOL)encrypted toMacName:(NSString *)macName;
- (void)sendWebViewURL:(NSString *)url;
- (void)processRemoteTableOfContents;
@end
#define DHRemotesChangedNotification @"DHRemotesChangedNotification"
================================================
FILE: Dash/DHRemoteServer.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRemoteServer.h"
#import "SAMKeychain.h"
#import "DHDocsetManager.h"
#import "DHRemoteImage.h"
#import "Reachability.h"
#import "DHDBResult.h"
#import "DHRemoteBrowser.h"
#import "DHNestedViewController.h"
#import "DHRemoteProtocol.h"
#import "DHCSS.h"
#import "DHTocBrowser.h"
@implementation DHRemoteServer
+ (DHRemoteServer *)sharedServer
{
static dispatch_once_t pred;
static DHRemoteServer *_server = nil;
dispatch_once(&pred, ^{
_server = [[DHRemoteServer alloc] init];
[_server setUp];
});
return _server;
}
- (void)setUp
{
[NSKeyedUnarchiver setClass:[DHDBResult class] forClassName:@"DHDBSnippetResult"];
[NSKeyedUnarchiver setClass:[DHDBResult class] forClassName:@"DHSearchEngineResult"];
[NSKeyedUnarchiver setClass:[DHDBResult class] forClassName:@"DHDBUnifiedResult"];
self.requestsQueue = [NSMutableDictionary dictionary];
self.remotes = [NSMutableArray array];
self.connections = [NSMutableDictionary dictionary];
self.lastDecryptFailDates = [NSMutableDictionary dictionary];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self startServer];
});
Reachability *reach = [Reachability reachabilityForLocalWiFi];
reach.reachableOnWWAN = NO;
reach.reachableBlock = ^(Reachability *theReach) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self stopServer];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self startServer];
});
});
};
[reach startNotifier];
}
- (void)startServer
{
if(self.server)
{
return;
}
self.server = [[DTBonjourServer alloc] initWithBonjourType:@"_dash._tcp"];
[self.server setDelegate:self];
if(![self.server start])
{
NSLog(@"Failed to start Bonjour server");
self.server = nil;
[NSTimer scheduledTimerWithTimeInterval:10.0f target:self selector:@selector(startServer) userInfo:nil repeats:NO];
}
}
- (void)stopServer
{
self.server.delegate = nil;
[self.server stop];
self.server = nil;
for(DTBonjourDataConnection *connection in [self.connections allValues])
{
[self connectionDidClose:connection];
}
}
- (void)connectionDidClose:(DTBonjourDataConnection *)connection
{
for(NSString *key in self.connections.allKeys)
{
if(self.connections[key] == connection)
{
DHRemote *remote = [DHRemote remoteWithName:key icon:nil];
NSInteger index = [self.remotes indexOfObject:remote];
if(index != NSNotFound)
{
[self.remotes removeObjectAtIndex:index];
[[NSNotificationCenter defaultCenter] postNotificationName:DHRemotesChangedNotification object:nil];
}
[self.connections removeObjectForKey:key];
}
}
}
- (void)sendWebViewURL:(NSString *)url
{
self.sendWebViewURLTimer = [self.sendWebViewURLTimer invalidateTimer];
if(self.connectedRemote)
{
url = [url stringByReplacingOccurrencesOfString:@"dash-tarix://" withString:@"file://"];
self.sendWebViewURLTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 block:^{
[self sendObject:@{@"url": url} forRequestName:@"syncWebViewURL" encrypted:YES toMacName:self.connectedRemote.name];
} repeats:NO];
}
}
- (void)processRemoteTableOfContents
{
DHWebViewController *controller = [DHWebViewController sharedWebViewController];
if(iPad && isRegularHorizontalClass)
{
if(controller.methodsPopover.popoverVisible)
{
[controller.methodsPopover dismissPopoverAnimated:YES];
}
}
else
{
[[controller.actualTOCBrowser searchDisplayController] setActive:NO animated:NO];
[[controller.actualTOCBrowser presentingViewController] dismissViewControllerAnimated:YES completion:nil];
}
controller.lastTocBrowser = nil;
controller.currentMethods = (self.tableOfContentsMethods.count) ? self.tableOfContentsMethods : nil;
controller.navigationItem.rightBarButtonItem = (self.tableOfContentsMethods.count) ? [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"tocMenu"] style:UIBarButtonItemStylePlain target:controller action:@selector(tocButtonPressed:)] : (self.tableOfContentsIsSnippet) ? [[UIBarButtonItem alloc] initWithTitle:@"Use" style:UIBarButtonItemStylePlain target:controller action:@selector(snippetUseButtonPressed:)] : nil;
}
- (BOOL)shouldIgnoreDueToDecryptFlood:(NSString *)macName
{
NSDate *date = self.lastDecryptFailDates[macName];
if(date && [[NSDate date] timeIntervalSinceDate:date] < 0.5)
{
return YES;
}
[self.lastDecryptFailDates removeObjectForKey:macName];
return NO;
}
- (void)bonjourServer:(DTBonjourServer *)server didReceiveObject:(NSDictionary *)dict onConnection:(DTBonjourDataConnection *)connection
{
@try {
NSString *macName = dict[@"name"];
NSString *requestName = dict[@"requestName"];
NSDictionary *userInfo = dict[@"userInfo"];
BOOL isEncrypted = [dict[@"encrypted"] boolValue];
NSArray *encryptedCheck = dict[@"encryptedCheck"];
if(!macName || !requestName || (isEncrypted && !encryptedCheck))
{
NSLog(@"Remote object receive failed validation");
return;
}
if(isEncrypted)
{
NSString *password = [SAMKeychain passwordForService:@"Dash Remote" account:macName];
BOOL success = NO;
if(password && password.length)
{
if(!connection.originAddress || !connection.originAddress.length)
{
success = YES;
}
else
{
for(NSData *data in encryptedCheck)
{
NSData *ipAddressData = [data AES256DecryptWithKey:password];
NSString *ipAddress = (ipAddressData) ? [[NSString alloc] initWithData:ipAddressData encoding:NSUTF8StringEncoding] : nil;
if(ipAddress && ipAddress.length && [ipAddress isCaseInsensitiveEqual:connection.originAddress])
{
success = YES;
break;
}
}
}
if(success && userInfo)
{
userInfo = (NSDictionary*)[(NSData*)userInfo AES256DecryptWithKey:password];
if(!userInfo)
{
success = NO;
}
}
}
if(!success)
{
self.lastDecryptFailDates[macName] = [NSDate date];
NSLog(@"decryption failed!");
return;
}
}
if([dict[@"gzipped"] boolValue])
{
userInfo = (userInfo) ? (id)[(id)userInfo gunzippedData] : nil;
}
userInfo = (userInfo) ? [NSKeyedUnarchiver unarchiveObjectWithData:(NSData*)userInfo] : nil;
NSLog(@"received %@", requestName);
#pragma mark Start handling requests
#pragma mark Enforce encrypted routes
NSArray *allowedUnencrypted = @[@"pairRequest", @"loadRequestResponse"];
if(!isEncrypted && ![allowedUnencrypted containsObject:requestName])
{
NSLog(@"Request %@ failed because not allowed without encryption", requestName);
return;
}
else if([self shouldIgnoreDueToDecryptFlood:macName])
{
#ifdef DEBUG
NSLog(@"Request %@ failed due to flood", requestName);
#endif
return;
}
self.connections[macName] = connection;
#pragma mark Route: pairRequest
if([requestName isEqualToString:@"pairRequest"] && !self.shownAlert.isVisible)
{
if(self.ignoreRequests)
{
return;
}
@try {
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
}
@catch(NSException *exception) { NSLog(@"%@ %@", exception, [exception callStackSymbols]); }
self.shownAlert = [UIAlertView showWithTitle:@"Pair Request Received" message:[NSString stringWithFormat:@"Dash on \"%@\" requested to pair with you. If you would like to pair, please enter the pair code:", macName] style:UIAlertViewStylePlainTextInput cancelButtonTitle:@"Cancel" otherButtonTitles:@[@"Pair", @"Ignore Future Requests"] tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
if(buttonIndex == 1)
{
NSString *code = [[alertView textFieldAtIndex:0] text];
code = (code) ? : @"";
NSError *error = nil;
if(![SAMKeychain setPassword:code forService:@"Dash Remote" account:macName error:&error])
{
NSLog(@"Couldn't save remote password in keychain: %@", error);
}
[self sendObject:nil forRequestName:@"pairComplete" encrypted:YES toMacName:macName];
}
else if(buttonIndex == 2)
{
self.ignoreRequests = YES;
}
}];
}
#pragma mark Route: pairHello
if([requestName isEqualToString:@"pairHello"])
{
[self sendObject:nil forRequestName:@"pairHello" encrypted:YES toMacName:macName];
}
#pragma mark Route: readyToConnect
if([requestName isEqualToString:@"readyToConnect"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:userInfo[@"icon"]];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
remote = self.connectedRemote;
[remote connect];
}
if(![self.remotes containsObject:remote])
{
[self.remotes addObject:remote];
[[NSNotificationCenter defaultCenter] postNotificationName:DHRemotesChangedNotification object:nil];
}
}
#pragma mark Route: unpair
if([requestName isEqualToString:@"unpair"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
self.connectedRemote = nil;
}
if([self.remotes containsObject:remote])
{
[self.remotes removeObject:remote];
[[NSNotificationCenter defaultCenter] postNotificationName:DHRemotesChangedNotification object:nil];
}
}
#pragma mark Route: syncResults
if([requestName isEqualToString:@"syncResults"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
DHRemoteBrowser *browser = self.connectedRemote.browser;
browser.results = userInfo[@"results"];
NSString *query = [[userInfo[@"searchQuery"] substringToString:@" "] trimWhitespace];
browser.title = [query length] ? query : self.connectedRemote.name;
[browser.tableView reloadData];
NSInteger row = [userInfo[@"selectedRow"] integerValue];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
if(isRegularHorizontalClass || browser.navigationController.visibleViewController != browser)
{
[browser.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
if(![userInfo[@"isFuzzyAppend"] boolValue])
{
if(isRegularHorizontalClass || browser.navigationController.visibleViewController != browser)
{
[browser.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:NO];
}
[browser popNestedViewControllers];
}
}
}
#pragma mark Route: syncSelectedRow
if([requestName isEqualToString:@"syncSelectedRow"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
DHRemoteBrowser *browser = self.connectedRemote.browser;
NSInteger row = [userInfo[@"selectedRow"] integerValue];
NSInteger indexOfActiveItem = [userInfo[@"indexOfActiveItem"] integerValue];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
if([browser.tableView numberOfRowsInSection:0] > row && browser.results.count > row)
{
if(![browser.tableView.indexPathForSelectedRow isEqual:indexPath])
{
if(isRegularHorizontalClass || browser.navigationController.visibleViewController != browser)
{
[browser.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[browser.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:NO];
}
[browser popNestedViewControllers];
}
DHDBResult *result = (browser.results)[row];
if(result.indexOfActiveItem != indexOfActiveItem)
{
[result setActiveItemByIndex:indexOfActiveItem];
DHNestedViewController *nestedController = [browser nestedViewController];
if(nestedController.result == result)
{
NSIndexPath *nestedIndexPath = [NSIndexPath indexPathForRow:indexOfActiveItem inSection:0];
if(isRegularHorizontalClass || nestedController.navigationController.visibleViewController != nestedController)
{
[nestedController.tableView selectRowAtIndexPath:nestedIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[nestedController.tableView scrollToRowAtIndexPath:nestedIndexPath atScrollPosition:UITableViewScrollPositionNone animated:NO];
}
}
}
}
}
}
#pragma mark Route: syncWebViewURL
if([requestName isEqualToString:@"syncWebViewURL"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
NSString *url = userInfo[@"url"];
NSString *hash = [url substringFromStringReturningNil:@"#"];
url = [url substringToString:@"#"];
url = [url stringByReplacingPercentEscapes];
if(hash && hash.length)
{
url = [url stringByAppendingFormat:@"#%@", hash];
}
DHWebViewController *webViewController = [DHWebViewController sharedWebViewController];
if([webViewController isViewLoaded])
{
[webViewController loadURL:url];
}
else
{
webViewController.result.remoteResultURL = url;
}
}
}
#pragma mark Route: loadRequestResponse
if([requestName isEqualToString:@"loadRequestResponse"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
NSString *identifier = userInfo[@"identifier"];
if(identifier && self.requestsQueue[identifier])
{
NSMutableData *data = userInfo[@"data"];
if([userInfo[@"gzipped"] boolValue])
{
data = (id)[data gunzippedData];
}
[self.requestsQueue[identifier] receivedData:data userInfo:userInfo[@"responseUserInfo"] isTimeout:NO];
}
}
}
#pragma mark Route: syncAppleLanguage
if([requestName isEqualToString:@"syncAppleLanguage"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
NSString *language = userInfo[@"language"];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if(![[defaults objectForKey:DHActiveAppleLanguageKey] isEqualToString:language])
{
[defaults setObject:language forKey:DHActiveAppleLanguageKey];
[[DHCSS sharedCSS] refreshActiveCSS];
[[DHWebViewController sharedWebViewController] reload];
}
}
}
#pragma mark Route: syncNewAppleLanguage
if([requestName isEqualToString:@"syncNewAppleLanguage"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
[DHAppleActiveLanguage setLanguage:[userInfo[@"language"] integerValue]];
}
}
#pragma mark Route: syncTableOfContents
if([requestName isEqualToString:@"syncTableOfContents"])
{
DHRemote *remote = [DHRemote remoteWithName:macName icon:nil];
if(self.connectedRemote && [self.connectedRemote isEqual:remote])
{
self.tableOfContentsMethods = userInfo[@"methods"];
self.tableOfContentsIsSnippet = [userInfo[@"isSnippet"] boolValue];
[self processRemoteTableOfContents];
}
}
}
@catch(NSException *exception) {
NSLog(@"Remote receive object exception: %@ %@", exception, [exception callStackSymbols]);
}
}
- (void)sendObject:(id)object forRequestName:(NSString *)name encrypted:(BOOL)encrypted toMacName:(NSString *)macName
{
if(!macName)
{
return;
}
DTBonjourDataConnection *connection = self.connections[macName];
if(connection && connection.isOpen)
{
NSLog(@"sent %@", name);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[@"requestName"] = name;
if(object)
{
object = [NSKeyedArchiver archivedDataWithRootObject:object];
NSData *gzipped = [object gzippedDataWithCompressionLevel:0.7];
object = (gzipped) ? gzipped : object;
if(gzipped)
{
dict[@"gzipped"] = @YES;
}
}
if(encrypted)
{
dict[@"encrypted"] = @(encrypted);
NSString *password = [self passwordForMacName:macName];
NSMutableArray *encryptedCheck = [NSMutableArray array];
dict[@"encryptedCheck"] = encryptedCheck;
for(NSString *host in [NSArray currentIPAddresses])
{
NSData *encryptedHost = [[[host substringToString:@"%"] dataUsingEncoding:NSUTF8StringEncoding] AES256EncryptWithKey:password];
if(encryptedHost)
{
[encryptedCheck addObject:encryptedHost];
}
}
if(object)
{
object = [object AES256EncryptWithKey:password];
}
}
if(object)
{
dict[@"userInfo"] = object;
}
[connection sendObject:dict error:nil];
}
}
- (NSString *)passwordForMacName:(NSString *)macName
{
return [SAMKeychain passwordForService:@"Dash Remote" account:macName];
}
@end
================================================
FILE: Dash/DHRepo.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHRepoTableViewCell.h"
#import "DHLatencyTester.h"
#import "MRProgress.h"
#import "DHDocsetIndexer.h"
#import "DHUnarchiver.h"
@interface DHRepo : UITableViewController
@property (strong) NSMutableArray *feeds;
@property (strong) NSMutableArray *filteredFeeds;
@property (assign) BOOL searchBarActive;
@property (assign) BOOL searchBarActiveIsALie;
@property (assign) BOOL didFirstReload;
@property (weak) UISearchDisplayController *searchController;
@property (strong) NSString *filterQuery;
@property (weak) MRProgressOverlayView *updateOverlay;
@property (assign) IBOutlet UISearchBar *searchBar;
@property (assign) BOOL loading;
@property (assign) NSString *loadingText;
- (void)setUp;
- (IBAction)downloadButtonPressed:(id)sender;
- (IBAction)uninstallButtonPressed:(id)sender;
- (IBAction)errorButtonPressed:(id)sender;
- (NSString *)installFeed:(DHFeed *)feed isAnUpdate:(BOOL)isAnUpdate;
- (BOOL)canInstallFeed:(DHFeed *)aFeed;
- (NSString *)docsetPathForFeed:(DHFeed *)feed;
- (NSString *)defaultsKey;
- (NSString *)uniqueTempDirAtPath:(NSString *)path;
- (void)saveState;
- (NSMutableArray *)activeFeeds;
- (IBAction)updateButtonPressed:(id)sender;
- (BOOL)alertIfUpdatesAreScheduled;
- (void)backgroundCheckForUpdatesIfNeeded;
- (NSString *)repoIdentifier; // Used to find a corresponding repo for a installed docset
- (NSString *)defaultsAutomaticallyCheckForUpdatesKey;
@property (nonatomic, strong, readonly, class) NSString *defaultsAlphabetizingKey;
- (void)emptyTrashAtPath:(NSString *)trashPath;
- (NSString *)docsetInstallFolderPath;
- (NSString *)uniqueTrashPath;
- (void)reload;
- (BOOL)shouldStall;
- (NSInteger)numberOfEntriesBeingInstalled;
- (NSInteger)indexOfFeedWithFeedURL:(NSString *)feedURL;
- (void)startInstallingFeed:(DHFeed *)feed isAnUpdate:(BOOL)isAnUpdate;
- (NSString *)defaultsScheduledUpdateKey;
- (void)updateFeeds:(NSArray *)feeds;
- (void)checkForUpdatesAndShowInterface:(BOOL)withInterface updateWithoutAsking:(BOOL)updateWithoutAsking;
@end
NSInteger compareFeeds(id feed1, id feed2, void *context);
#define DHSettingsChangedNotification @"DHSettingsChangedNotification"
================================================
FILE: Dash/DHRepo.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRepo.h"
#import "DHFeed.h"
#import "DHDBResult.h"
#import "DHDocsetManager.h"
#import "DHDocsetTransferrer.h"
#import "DHCheatRepo.h"
#import "DHRightDetailLabel.h"
@implementation DHRepo
- (void)setUp // doesn't get called unless you call the singleton from DHAppDelegate
{
// Don't count on anything being loaded here. Only stateless stuff.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *file = nil;
NSString *installPath = [self docsetInstallFolderPath];
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:installPath];
NSMutableArray *toDelete = [NSMutableArray array];
while(file = [dirEnum nextObject])
{
if([[file pathExtension] isCaseInsensitiveEqual:@"docset"])
{
[dirEnum skipDescendants];
continue;
}
if([[file lastPathComponent] hasPrefix:@"dash_temp_"])
{
[dirEnum skipDescendants];
NSString *fullPath = [installPath stringByAppendingPathComponent:file];
[toDelete addObject:fullPath];
}
}
for(NSString *fullPath in toDelete)
{
[fileManager moveItemAtPath:fullPath toPath:[self uniqueTrashPath] error:nil];
}
[self emptyTrashAtPath:[self trashPath]];
});
[MRCircularProgressView appearance].lineWidth = 2.0f;
[MRCircularProgressView appearance].borderWidth = 1.0f;
}
- (IBAction)updateButtonPressed:(id)sender
{
if(self.loading)
{
[[[UIAlertView alloc] initWithTitle:@"Loading..." message:@"Wait for loading to complete and try again." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
return;
}
[self checkForUpdatesAndShowInterface:YES updateWithoutAsking:NO];
}
- (void)checkForUpdatesAndShowInterface:(BOOL)withInterface updateWithoutAsking:(BOOL)updateWithoutAsking
{
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:[self defaultsScheduledUpdateKey]];
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:[self defaultsUpdateLastCheckDateKey]];
MRProgressOverlayView *overlay = nil;
if(withInterface)
{
BOOL found = NO;
for(DHFeed *feed in self.feeds)
{
if(feed.installed)
{
found = YES;
break;
}
}
if(!found)
{
[[[UIAlertView alloc] initWithTitle:@"Nothing to Update" message:[NSString stringWithFormat:@"You don't have any %@ installed. Download some!", [self isKindOfClass:[DHCheatRepo class]] ? @"cheat sheets" : @"docsets"] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
return;
}
overlay = [MRProgressOverlayView showOverlayAddedTo:(isRegularHorizontalClass) ? self.splitViewController.view : self.navigationController.view title:@"Checking..." mode:MRProgressOverlayViewModeIndeterminate animated:YES stopBlock:^(MRProgressOverlayView *progressOverlayView) {
self.updateOverlay = nil;
[progressOverlayView dismiss:YES];
}];
self.updateOverlay = overlay;
}
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
if(withInterface)
{
[NSThread sleepForTimeInterval:1.0f];
}
while(self.loading)
{
[NSThread sleepForTimeInterval:1.0f];
}
NSMutableArray *toUpdate = [NSMutableArray array];
for(DHFeed *feed in self.feeds)
{
if(withInterface && self.updateOverlay != overlay)
{
return;
}
if(feed.installed && !feed.installing)
{
NSString *error = nil;
DHFeedResult *result = [self loadFeed:feed error:&error];
if(result && ![result.version isEqualToString:feed.installedVersion])
{
[toUpdate addObject:feed];
}
}
}
[toUpdate filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
return [evaluatedObject installed] && ![evaluatedObject installing];
}]];
dispatch_sync(dispatch_get_main_queue(), ^{
if(withInterface)
{
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:[self defaultsScheduledUpdateKey]];
if(overlay == self.updateOverlay)
{
if(!toUpdate.count)
{
[overlay setMode:MRProgressOverlayViewModeCheckmark];
[overlay setTitleLabelText:@"Up to date"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[overlay dismiss:YES];
});
}
else
{
[overlay dismiss:YES completion:^{
[self showUpdateRequestForFeeds:toUpdate count:toUpdate.count docsetList:[self docsetListForFeeds:toUpdate]];
}];
}
}
return;
}
else if(toUpdate.count && !withInterface)
{
if(updateWithoutAsking)
{
[self updateFeeds:toUpdate];
}
else
{
[[NSUserDefaults standardUserDefaults] setInteger:toUpdate.count forKey:[self defaultsScheduledUpdateKey]];
[[NSUserDefaults standardUserDefaults] setObject:[self docsetListForFeeds:toUpdate] forKey:[self defaultsScheduledDocsetListUpdateKey]];
}
}
else if(!toUpdate.count)
{
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:[self defaultsScheduledUpdateKey]];
}
});
});
}
- (void)showUpdateRequestForFeeds:(NSArray *)toUpdate count:(NSInteger)feedCount docsetList:(NSString *)docsetList
{
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:[self defaultsScheduledUpdateKey]];
[UIAlertView showWithTitle:@"Updates Found" message:[NSString stringWithFormat:@"Updates are available for %ld %@:%@%@", (long)feedCount, (feedCount > 1) ? @"docsets" : @"docset", (feedCount > 1) ? @"\n\n" : @" ", docsetList] cancelButtonTitle:@"Maybe Later" otherButtonTitles:@[@"Update"] tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
if(buttonIndex != alertView.cancelButtonIndex)
{
if(toUpdate)
{
[self updateFeeds:toUpdate];
}
else
{
[self checkForUpdatesAndShowInterface:NO updateWithoutAsking:YES];
}
}
}];
}
- (NSString *)docsetListForFeeds:(NSArray *)toUpdate
{
NSMutableArray *sortedFeeds = [NSMutableArray arrayWithArray:toUpdate];
[sortedFeeds sortUsingFunction:compareFeeds context:nil];
NSMutableString *docsetList = [[NSMutableString alloc] init];
NSInteger count = 0;
for(DHFeed *feed in sortedFeeds)
{
if(docsetList.length)
{
[docsetList appendString:@"\n"];
}
[docsetList appendString:[feed docsetNameWithVersion:NO]];
++count;
if(count > 15)
{
[docsetList appendString:@"\nand more..."];
break;
}
}
return docsetList;
}
- (void)updateFeeds:(NSArray *)feeds
{
for(DHFeed *feed in feeds)
{
if(feed.installed && !feed.installing)
{
[self startInstallingFeed:feed isAnUpdate:YES];
}
}
}
- (BOOL)alertIfUpdatesAreScheduled
{
NSInteger count = [[NSUserDefaults standardUserDefaults] integerForKey:[self defaultsScheduledUpdateKey]];
if(count > 0)
{
NSString *docsetList = [[NSUserDefaults standardUserDefaults] objectForKey:[self defaultsScheduledDocsetListUpdateKey]];
if(docsetList && docsetList.length)
{
[self showUpdateRequestForFeeds:nil count:count docsetList:docsetList];
return YES;
}
}
return NO;
}
- (void)backgroundCheckForUpdatesIfNeeded
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL shouldUpdate = [defaults boolForKey:[self defaultsAutomaticallyCheckForUpdatesKey]];
NSDate *lastDate = [defaults objectForKey:[self defaultsUpdateLastCheckDateKey]];
if(shouldUpdate && (!lastDate || [[NSDate date] timeIntervalSinceDate:lastDate] > 60*60*24))
{
[self checkForUpdatesAndShowInterface:NO updateWithoutAsking:NO];
}
}
- (DHFeedResult *)loadFeed:(DHFeed *)feed error:(NSString **)returnError
{
return nil;
}
- (IBAction)downloadButtonPressed:(id)sender
{
NSUInteger row = [sender tag];
DHFeed *feed = [self activeFeeds][row];
if([self canInstallFeed:feed])
{
[self startInstallingFeed:feed isAnUpdate:NO];
}
}
- (void)showDownloadProgressViewForFeed:(DHFeed *)feed
{
DHRepoTableViewCell *cell = feed.cell;
feed.error = nil;
cell.progressView.progress = 0.0;
cell.progressView.transform = CGAffineTransformMakeScale(0.01, 0.01);
[UIView animateWithDuration:0.3 animations:^{
cell.checkmark.alpha = 0.0;
cell.checkmark.transform = CGAffineTransformMakeScale(0.01, 0.01);
cell.uninstallButton.alpha = 0.0;
cell.uninstallButton.transform = CGAffineTransformMakeScale(0.01, 0.01);
cell.errorButton.alpha = 0.0;
cell.errorButton.transform = CGAffineTransformMakeScale(0.01, 0.01);
cell.progressView.alpha = 1.0;
cell.progressView.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.downloadButton.alpha = 0.0;
cell.downloadButton.transform = CGAffineTransformMakeScale(0.01, 0.01);
} completion:^(BOOL finished) {
cell.downloadButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.errorButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.checkmark.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.uninstallButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
[feed adjustTitleLabelWidthBasedOnButtonsShown];
}];
}
- (IBAction)errorButtonPressed:(id)sender
{
NSUInteger row = [sender tag];
DHFeed *feed = [self activeFeeds][row];
[[[UIAlertView alloc] initWithTitle:@"Docset Install Failed" message:[NSString stringWithFormat:@"%@ Please check your Internet connection and available free space and try again.", feed.error] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
- (void)showErrorButtonForFeed:(DHFeed *)feed
{
DHRepoTableViewCell *cell = feed.cell;
feed.detailString = @"";
feed.maxRightDetailWidth = 0.0;
cell.titleLabel.rightDetailText = @"";
cell.errorButton.transform = CGAffineTransformMakeScale(0.01, 0.01);
[feed adjustTitleLabelWidthBasedOnButtonsShown];
[UIView animateWithDuration:0.3 animations:^{
cell.errorButton.alpha = 1.0;
cell.errorButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
} completion:nil];
}
- (void)showUninstallButtonForFeed:(DHFeed *)feed
{
DHRepoTableViewCell *cell = feed.cell;
cell.uninstallButton.transform = CGAffineTransformMakeScale(0.01, 0.01);
cell.checkmark.transform = CGAffineTransformMakeScale(0.01, 0.01);
feed.detailString = @"";
feed.maxRightDetailWidth = 0.0;
cell.titleLabel.rightDetailText = @"";
[feed adjustTitleLabelWidthBasedOnButtonsShown];
feed.progressShown = NO;
[UIView animateWithDuration:0.3 animations:^{
cell.uninstallButton.alpha = 1.0;
cell.uninstallButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.checkmark.alpha = 1.0;
cell.checkmark.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.progressView.alpha = 0.0;
cell.progressView.transform = CGAffineTransformMakeScale(0.01, 0.01);
} completion:^(BOOL finished) {
cell.progressView.progress = 0.0;
cell.progressView.transform = CGAffineTransformMakeScale(1.0, 1.0);
}];
}
- (IBAction)stopButtonPressed:(id)sender
{
NSUInteger row = [sender tag];
DHFeed *feed = [self activeFeeds][row];
feed.installing = NO;
[feed.feedResult.fileDownload cancelDownload];
[self feedWasStopped:feed];
feed.feedResult = nil;
feed.progressShown = NO;
feed.progress = 0.0;
feed.detailString = @"";
feed.maxRightDetailWidth = 0.0;
feed.cell.titleLabel.rightDetailText = @"";
if(feed.installed)
{
[self setTitle:[feed docsetNameWithVersion:YES] forCell:feed.cell];
[self showUninstallButtonForFeed:feed];
}
else
{
[self showDownloadButtonForFeed:feed];
}
}
- (void)feedWasStopped:(DHFeed *)feed
{
}
- (IBAction)uninstallButtonPressed:(id)sender
{
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:[self defaultsScheduledUpdateKey]];
NSUInteger row = [sender tag];
DHFeed *feed = [self activeFeeds][row];
feed.installed = NO;
feed.installedVersion = nil;
[self saveState];
[self setTitle:[feed docsetNameWithVersion:YES] forCell:feed.cell];
NSString *trashPath = [self uniqueTrashPath];
NSString *feedPath = [self docsetPathForFeed:feed];
NSFileManager *fileManager = [NSFileManager defaultManager];
[[DHDocsetManager sharedManager] removeDocsetsInFolder:feedPath];
[fileManager moveItemAtPath:feedPath toPath:trashPath error:nil];
[self feedDidUninstall:feed];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[self emptyTrashAtPath:trashPath];
});
[self showDownloadButtonForFeed:feed];
}
- (void)feedDidUninstall:(DHFeed *)feed
{
}
- (void)emptyTrashAtPath:(NSString *)trashPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
@autoreleasepool {
NSString *file = nil;
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:trashPath];
NSMutableArray *toDelete = [NSMutableArray array];
while(file = [dirEnum nextObject])
{
if([[file pathExtension] isCaseInsensitiveEqual:@"docset"])
{
[dirEnum skipDescendants];
NSString *fullPath = [trashPath stringByAppendingPathComponent:file];
[toDelete addObject:[fullPath stringByAppendingPathComponent:@"Contents/Resources/tarix.tgz"]];
[toDelete addObject:[fullPath stringByAppendingPathComponent:@"Contents/Resources/optimisedIndex.dsidx"]];
[toDelete addObject:[fullPath stringByAppendingPathComponent:@"Contents/Resources/docSet.dsidx"]];
continue;
}
if([[file pathExtension] isCaseInsensitiveEqual:@"dsidx"] || [[file pathExtension] isCaseInsensitiveEqual:@"tgz"] || [[file pathExtension] isCaseInsensitiveEqual:@"tarix"])
{
NSString *fullPath = [trashPath stringByAppendingPathComponent:file];
[toDelete addObject:fullPath];
}
}
for(NSString *path in toDelete)
{
[fileManager removeItemAtPath:path error:nil];
}
}
[fileManager removeItemAtPath:trashPath error:nil];
}
- (void)showDownloadButtonForFeed:(DHFeed *)feed
{
DHRepoTableViewCell *cell = feed.cell;
cell.downloadButton.transform = CGAffineTransformMakeScale(0.01, 0.01);
feed.detailString = @"";
feed.maxRightDetailWidth = 0.0;
feed.cell.titleLabel.rightDetailText = @"";
feed.progressShown = NO;
[UIView animateWithDuration:0.3 animations:^{
cell.downloadButton.alpha = 1.0;
cell.downloadButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.progressView.alpha = 0.0;
cell.progressView.transform = CGAffineTransformMakeScale(0.01, 0.01);
cell.checkmark.alpha = 0.0;
cell.checkmark.transform = CGAffineTransformMakeScale(0.01, 0.01);
cell.uninstallButton.alpha = 0.0;
cell.uninstallButton.transform = CGAffineTransformMakeScale(0.01, 0.01);
} completion:^(BOOL finished) {
cell.progressView.progress = 0.0;
cell.progressView.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.uninstallButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
cell.checkmark.transform = CGAffineTransformMakeScale(1.0, 1.0);
[feed adjustTitleLabelWidthBasedOnButtonsShown];
}];
}
- (UITableView *)activeTableView
{
return (self.searchBarActive) ? self.searchController.searchResultsTableView : self.tableView;
}
- (NSMutableArray *)activeFeeds
{
return (self.searchBarActive) ? self.filteredFeeds : self.feeds;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return (self.loading) ? 1 : ([self activeFeeds].count) ? 1 : 0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(self.loading)
{
return 3;
}
if(tableView != self.tableView)
{
return self.filteredFeeds.count;
}
return self.feeds.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(self.loading)
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHLoadingCell" forIndexPath:indexPath];
cell.userInteractionEnabled = NO;
if(indexPath.row == 2)
{
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraph setAlignment:NSTextAlignmentCenter];
UIFont *font = [UIFont boldSystemFontOfSize:20];
cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:(self.loadingText) ? self.loadingText : @"Loading..." attributes:@{NSParagraphStyleAttributeName : paragraph, NSForegroundColorAttributeName: [UIColor colorWithWhite:0.8 alpha:1], NSFontAttributeName: font}];
}
else
{
cell.textLabel.text = @"";
}
return cell;
}
DHRepoTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHRepoCell" forIndexPath:indexPath];
[cell.downloadButton setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -2, -10, -10)];
[cell.uninstallButton setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -10)];
[cell.progressView setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -10)];
[cell.errorButton setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -6)];
[cell setTagsToIndex:indexPath.row];
NSArray *targetArray = (tableView != self.tableView) ? self.filteredFeeds : self.feeds;
DHFeed *feed = targetArray[indexPath.row];
cell.feed = feed;
[cell.progressView.stopButton addTarget:self action:@selector(stopButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
[cell.errorButton addTarget:self action:@selector(errorButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
[cell.downloadButton addTarget:self action:@selector(downloadButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
[cell.uninstallButton addTarget:self action:@selector(uninstallButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
cell.titleLabel.opaque = NO;
// cell.checkmark.image = [cell.checkmark.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
cell.titleLabel.backgroundColor = [UIColor clearColor];
cell.platform.image = feed.icon;
[feed prepareCell:cell];
cell.titleLabel.maxRightDetailWidth = feed.maxRightDetailWidth;
cell.titleLabel.rightDetailText = feed.detailString;
cell.titleLabel.subtitle = feed.authorLinkText;
cell.titleLabel.authorLinkHref = feed.authorLinkHref;
[self setSizeLabelForCell:cell];
[self setTitle:[feed docsetNameWithVersion:!feed.installing] forCell:cell];
return cell;
}
- (void)setSizeLabelForCell:(DHRepoTableViewCell *)cell
{
if(cell.feed.installed && !cell.feed.installing && cell.feed.size && cell.feed.size.length)
{
if(!isRegularHorizontalClass)
{
[cell.titleLabel setRightDetailText:nil];
return;
}
NSString *label = [cell.feed.size stringByAppendingString:@""];
cell.titleLabel.maxRightDetailWidth = [DHRightDetailLabel calculateMaxDetailWidthBasedOnLongestPossibleString:label];
[cell.titleLabel setRightDetailText:label];
}
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(DHRepoTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if([cell isKindOfClass:[DHRepoTableViewCell class]])
{
cell.feed.cell = nil;
cell.feed = nil;
}
}
- (void)highlightCell:(DHRepoTableViewCell *)cell
{
NSRange range;
NSInteger offset = 0;
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:cell.titleLabel.attributedText];
for(NSString *key in [DHDBResult highlightDictionary])
{
[string removeAttribute:key range:NSMakeRange(0, string.length)];
}
NSString *substring = [[string string] copy];
BOOL didAddAttributes = NO;
while((range = [substring rangeOfString:self.filterQuery options:NSCaseInsensitiveSearch]).location != NSNotFound)
{
[string addAttributes:[DHDBResult highlightDictionary] range:NSMakeRange(range.location+offset, range.length)];
substring = [substring substringFromDashIndex:range.location+range.length];
offset += range.location+range.length;
didAddAttributes = YES;
}
if(didAddAttributes)
{
cell.titleLabel.attributedText = string;
}
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
if(self.searchBarActive || (self.searchBarActiveIsALie && !self.searchBarActive))
{
return nil;
}
return [self.feeds characterIndexTitles];
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)aIndex
{
NSInteger index = [self.feeds indexOfFirstObjectThatStartsWithCharacter:title];
if(index != 0)
{
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
return index;
}
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView
{
if(isIOS11)
{
if(@available(iOS 11.0, *))
{
tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
[controller.searchResultsTableView registerNib:[UINib nibWithNibName:@"DHRepoCell" bundle:nil] forCellReuseIdentifier:@"DHRepoCell"];
[controller.searchResultsTableView registerNib:[UINib nibWithNibName:@"DHLoadingCell" bundle:nil] forCellReuseIdentifier:@"DHLoadingCell"];
tableView.allowsSelection = NO;
}
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
controller.searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.searchController = controller;
self.searchBarActive = YES;
[self.tableView reloadSectionIndexTitles];
}
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
self.searchBarActive = NO;
[self reload];
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
[self filterFeedsWithQuery:searchString];
return YES;
}
- (void)searchDisplayController:(UISearchDisplayController *)controller willHideSearchResultsTableView:(UITableView *)tableView
{
if(!self.searchBarActive)
{
return;
}
self.searchBarActive = NO;
self.searchBarActiveIsALie = YES;
[self reload];
self.searchBarActiveIsALie = NO;
self.searchBarActive = YES;
}
- (void)reload
{
[self.tableView reloadData];
}
- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller
{
self.searchBarActive = NO;
}
- (void)filterFeedsWithQuery:(NSString *)query
{
self.filterQuery = query;
self.filteredFeeds = [NSMutableArray array];
NSMutableArray *aliasMatches = [NSMutableArray array];
for(DHFeed *feed in self.feeds)
{
if([[feed docsetNameWithVersion:!feed.installing] contains:query])
{
[self.filteredFeeds addObject:feed];
}
else
{
for(NSString *alias in feed.aliases)
{
if([alias contains:query])
{
[aliasMatches addObject:feed];
break;
}
}
}
}
[self.filteredFeeds sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSString *myName = [obj1 docsetNameWithVersion:![obj1 installing]];
NSString *theirName = [obj2 docsetNameWithVersion:![obj2 installing]];
NSRange myMatch = [myName rangeOfString:query options:NSCaseInsensitiveSearch];
NSRange theirMatch = [theirName rangeOfString:query options:NSCaseInsensitiveSearch];
if(myMatch.location < theirMatch.location)
{
return NSOrderedAscending;
}
else if(myMatch.location > theirMatch.location)
{
return NSOrderedDescending;
}
else
{
return [[obj1 sortName] localizedCaseInsensitiveCompare:[obj2 sortName]];
}
}];
[self.filteredFeeds addObjectsFromArray:aliasMatches];
}
- (void)setTitle:(NSString *)title forCell:(DHRepoTableViewCell *)cell
{
cell.titleLabel.text = title;
if(self.searchBarActive && self.filterQuery.length)
{
[self highlightCell:cell];
}
}
- (void)feedWillInstall:(DHFeed *)feed
{
}
- (void)startInstallingFeed:(DHFeed *)feed isAnUpdate:(BOOL)isAnUpdate
{
[self showDownloadProgressViewForFeed:feed];
feed.progress = 0.0;
feed.progressShown = YES;
feed.installing = YES;
feed.checkingForUpdates = NO;
feed.error = nil;
[self setTitle:[feed docsetNameWithVersion:NO] forCell:feed.cell];
[self feedWillInstall:feed];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:0.2f]; // without this there's a display glitch when internet is off and instant error is shown
NSString *result = [self installFeed:feed isAnUpdate:isAnUpdate];
dispatch_sync(dispatch_get_main_queue(), ^{
if(!feed.installing || [result isEqualToString:@"cancelled"])
{
// user cancelled install
return;
}
feed.installing = NO;
NSString *version = feed.feedResult.version;
feed.feedResult = nil;
feed.progress = 0.0;
if(!result)
{
feed.installed = YES;
feed.installedVersion = version;
[self saveState];
[self setTitle:[feed docsetNameWithVersion:YES] forCell:feed.cell];
[self showUninstallButtonForFeed:feed];
[self setSizeLabelForCell:feed.cell];
[feed adjustTitleLabelWidthBasedOnButtonsShown];
DHDocset *docset = [DHDocset firstDocsetInsideFolder:[self docsetPathForFeed:feed]];
docset.repoIdentifier = [self repoIdentifier];
docset.feedIdentifier = [feed uniqueIdentifier];
[[DHDocsetManager sharedManager] addDocset:docset andRemoveOthers:YES removeOnlyEqualPaths:[self isKindOfClass:[DHDocsetTransferrer class]]];
}
else if(isAnUpdate)
{
[self setTitle:[feed docsetNameWithVersion:YES] forCell:feed.cell];
[self showUninstallButtonForFeed:feed];
[feed adjustTitleLabelWidthBasedOnButtonsShown];
}
else
{
feed.error = result;
[self showErrorButtonForFeed:feed];
[self showDownloadButtonForFeed:feed];
[feed adjustTitleLabelWidthBasedOnButtonsShown];
}
});
});
}
- (NSString *)installFeed:(DHFeed *)feed isAnUpdate:(BOOL)isAnUpdate
{
NSLog(@"installFeed: not implemented for %@", NSStringFromClass([self class]));
return nil;
}
- (BOOL)canInstallFeed:(DHFeed *)feed
{
return YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerNib:[UINib nibWithNibName:@"DHRepoCell" bundle:nil] forCellReuseIdentifier:@"DHRepoCell"];
[self.tableView registerNib:[UINib nibWithNibName:@"DHLoadingCell" bundle:nil] forCellReuseIdentifier:@"DHLoadingCell"];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self traitCollectionDidChange:nil];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
if(self.searchController.isActive)
{
@try {
[self.searchController setActive:NO animated:NO];
} @catch (NSException *exception) {
}
}
if(isRegularHorizontalClass)
{
[self.navigationItem setHidesBackButton:YES animated:NO];
[self.tableView reloadData];
}
else
{
[self.navigationItem setHidesBackButton:NO animated:NO];
[self.tableView reloadData];
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIApplication sharedApplication].idleTimerDisabled = YES;
[[DHLatencyTester sharedLatency] performTests:NO];
[self traitCollectionDidChange:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[UIApplication sharedApplication].idleTimerDisabled = NO;
}
- (NSString *)docsetInstallFolderName
{
NSLog(@"docsetInstallFolderName not implemented for %@", NSStringFromClass([self class]));
return @"Unknown";
}
- (NSString *)repoIdentifier // Used to find a corresponding repo for a installed docset
{
return [self docsetInstallFolderName];
}
- (NSString *)docsetInstallFolderPath
{
return [[homePath stringByAppendingPathComponent:@"Docsets"] stringByAppendingPathComponent:[self docsetInstallFolderName]];
}
- (NSString *)docsetPathForFeed:(DHFeed *)feed
{
return [[self docsetInstallFolderPath] stringByAppendingPathComponent:feed.installFolderName];
}
- (NSString *)defaultsKey
{
return NSStringFromClass([self class]);
}
- (NSString *)defaultsScheduledUpdateKey
{
return [[self defaultsKey] stringByAppendingString:@"ScheduledUpdate"];
}
- (NSString *)defaultsScheduledDocsetListUpdateKey
{
return [[self defaultsKey] stringByAppendingString:@"ScheduledUpdateDocsetList"];
}
- (NSString *)defaultsAutomaticallyCheckForUpdatesKey
{
return @"AutomaticallyCheckForUpdates";
}
+ (NSString *)defaultsAlphabetizingKey {
return @"DocSetAlphabetizing";
}
- (NSString *)defaultsUpdateLastCheckDateKey
{
return [[self defaultsKey] stringByAppendingString:@"LastUpdateCheck"];
}
- (NSString *)trashPath
{
return [[self docsetInstallFolderPath] stringByAppendingPathComponent:@".Trash"];
}
- (NSString *)uniqueTrashPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *trash = [self trashPath];
[fileManager createDirectoryAtPath:trash withIntermediateDirectories:YES attributes:nil error:nil];
for(int i = 0; i < 500; i++)
{
NSString *random = [trash stringByAppendingPathComponent:[NSString randomStringWithLength:8]];
if(![fileManager fileExistsAtPath:random])
{
return random;
}
}
return [trash stringByAppendingPathComponent:[NSString randomStringWithLength:8]];
}
- (NSString *)uniqueTempDirAtPath:(NSString *)path
{
NSFileManager *fileManager = [NSFileManager defaultManager];
for(int i = 0; i < 100; i++)
{
NSString *random = [path stringByAppendingPathComponent:[@"dash_temp_" stringByAppendingString:[NSString randomStringWithLength:8]]];
if(![fileManager fileExistsAtPath:random])
{
return random;
}
}
return [path stringByAppendingPathComponent:[@"dash_temp_" stringByAppendingString:[NSString randomStringWithLength:8]]];
}
- (void)saveState
{
NSMutableArray *feeds = [NSMutableArray array];
for(DHFeed *feed in self.feeds)
{
[feeds addObject:[feed dictionaryRepresentation]];
}
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:feeds forKey:[self defaultsKey]];
}
- (BOOL)shouldStall
{
return [self numberOfEntriesBeingInstalled] > 2;
}
- (NSInteger)numberOfEntriesBeingInstalled
{
NSInteger count = 0;
for(DHFeed *feed in self.feeds)
{
if(feed.installing && !feed.waiting)
{
++count;
}
}
return count;
}
- (NSInteger)indexOfFeedWithFeedURL:(NSString *)feedURL
{
NSInteger i = 0;
for(DHFeed *feed in self.feeds)
{
if([feed.feedURL isEqualToString:feedURL])
{
return i;
}
++i;
}
return NSNotFound;
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder];
[self viewDidLoad];
}
@end
NSInteger compareFeeds(id feed1, id feed2, void *context)
{
NSString *myName = [feed1 sortName];
NSString *theirName = [feed2 sortName];
return [myName localizedCaseInsensitiveCompare:theirName];
}
================================================
FILE: Dash/DHRepoCell.xib
================================================
================================================
FILE: Dash/DHRepoTableView.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHRepoTableView : UITableView
@end
================================================
FILE: Dash/DHRepoTableView.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRepoTableView.h"
@implementation DHRepoTableView
@end
================================================
FILE: Dash/DHRepoTableViewCell.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "MRProgress.h"
#import "DHRightDetailLabel.h"
@class DHFeed;
@interface DHRepoTableViewCell : UITableViewCell
@property (assign) IBOutlet UIButton *downloadButton;
@property (assign) IBOutlet MRCircularProgressView *progressView;
@property (assign) IBOutlet UIButton *errorButton;
@property (assign) IBOutlet DHRightDetailLabel *titleLabel;
@property (assign) IBOutlet UIButton *uninstallButton;
@property (assign) IBOutlet UIImageView *checkmark;
@property (assign) IBOutlet UIImageView *platform;
@property (weak) DHFeed *feed;
- (void)setTagsToIndex:(NSInteger)index;
@end
================================================
FILE: Dash/DHRepoTableViewCell.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRepoTableViewCell.h"
@implementation DHRepoTableViewCell
- (void)setTagsToIndex:(NSInteger)index
{
self.downloadButton.tag = index;
self.progressView.stopButton.tag = index;
self.errorButton.tag = index;
self.uninstallButton.tag = index;
}
- (NSString *)accessibilityValue
{
return [self.titleLabel accessibilityValue];
}
@end
================================================
FILE: Dash/DHRightDetailLabel.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHRightDetailLabel : UILabel
@property (strong) NSString *_rightDetailText;
@property (assign, nonatomic) CGFloat maxRightDetailWidth;
@property (assign) BOOL isBrowserCell;
@property (strong) NSString *subtitle;
@property (strong) NSString *authorLinkHref;
- (void)setRightDetailText:(NSString *)rightDetailText;
+ (CGFloat)calculateMaxDetailWidthBasedOnLongestPossibleString:(NSString *)string;
- (void)setRightDetailText:(NSString *)rightDetailText adjustMainWidth:(BOOL)adjustWidth;
@end
================================================
FILE: Dash/DHRightDetailLabel.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRightDetailLabel.h"
@implementation DHRightDetailLabel
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if(self._rightDetailText.length)
{
NSMutableParagraphStyle *paragraph = NSMutableParagraphStyle.new;
paragraph.alignment = NSTextAlignmentRight;
rect = self.bounds;
if(self.isBrowserCell)
{
rect.origin.y += 11;
rect.size.width -= 2;
if(isRetina)
{
rect.origin.y -= 0.5;
}
[self._rightDetailText drawInRect:rect withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSParagraphStyleAttributeName: paragraph, NSForegroundColorAttributeName: [UIColor colorWithRed:142.0/255.0 green:142.0/255.0 blue:147.0/255.0 alpha:1.0]}];
}
else
{
rect.origin.y += 15;
rect.size.width -= 2;
[self._rightDetailText drawInRect:rect withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:12], NSParagraphStyleAttributeName: paragraph, NSForegroundColorAttributeName: [UIColor colorWithWhite:142.0/255.0 alpha:1.0]}];
}
}
if(self.subtitle.length)
{
NSMutableParagraphStyle *paragraph = NSMutableParagraphStyle.new;
paragraph.lineBreakMode = NSLineBreakByTruncatingTail;
rect = self.bounds;
rect.origin.y += 24;
rect.size.width -= self.maxRightDetailWidth;
NSString *subtitle = self.subtitle;
if(self.authorLinkHref)
{
subtitle = [@"Contributed by " stringByAppendingString:self.subtitle];
// Make author part underlined, commented out because making author clickable is a bit hard, giving up for now
// NSMutableAttributedString *attributedSubtitle = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Contributed by %@", self.subtitle]];
// [attributedSubtitle setAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:11], NSForegroundColorAttributeName: [UIColor colorWithRed:142.0/255.0 green:142.0/255.0 blue:147.0/255.0 alpha:1.0]} range:NSMakeRange(0, attributedSubtitle.length)];
// [attributedSubtitle addAttributes:@{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle)} range:NSMakeRange(@"Contributed by ".length, self.subtitle.length)];
// [attributedSubtitle drawInRect:rect];
}
// else
// {
[subtitle drawInRect:rect withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:11], NSParagraphStyleAttributeName: paragraph, NSForegroundColorAttributeName: [UIColor colorWithRed:142.0/255.0 green:142.0/255.0 blue:147.0/255.0 alpha:1.0]}];
// }
}
}
- (void)drawTextInRect:(CGRect)rect
{
if(self._rightDetailText.length)
{
rect = CGIncreaseRect(rect, 0, 0, -self.maxRightDetailWidth-12, 0);
}
if(self.subtitle.length)
{
rect = CGIncreaseRect(rect, 0, -7, 0, 0);
}
[super drawTextInRect:rect];
}
- (void)setRightDetailText:(NSString *)rightDetailText adjustMainWidth:(BOOL)adjustWidth
{
[self setRightDetailText:rightDetailText];
if(adjustWidth)
{
self.maxRightDetailWidth = [rightDetailText attributedSizeWithFont:[UIFont systemFontOfSize:(self.isBrowserCell) ? 16 : 12]].width;
}
}
- (void)setRightDetailText:(NSString *)rightDetailText
{
self._rightDetailText = rightDetailText;
if(!rightDetailText.length)
{
self.maxRightDetailWidth = 0.0;
}
[self setNeedsDisplay];
}
+ (CGFloat)calculateMaxDetailWidthBasedOnLongestPossibleString:(NSString *)string
{
CGSize size = [string attributedSizeWithFont:[UIFont systemFontOfSize:12]];
return size.width;
}
- (NSString *)accessibilityValue
{
return [[NSString stringWithFormat:@"%@ %@", self.subtitle ?: @"", self._rightDetailText ?: @""] trimWhitespace];
}
@end
================================================
FILE: Dash/DHSearchDisplayController.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHSearchDisplayController : UISearchDisplayController
@end
================================================
FILE: Dash/DHSearchDisplayController.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHSearchDisplayController.h"
#import "DHNestedViewController.h"
#import "JGMethodSwizzler.h"
@interface UISearchDisplayController (DHUtils)
- (void)navigationControllerWillShowViewController:(id)navigationController;
@end
@implementation UISearchDisplayController (DHUtils)
@end
@implementation DHSearchDisplayController
- (void)navigationControllerWillShowViewController:(id)navigationController
{
if(isRegularHorizontalClass)
{
SEL selector = NSSelectorFromString(@"_deselectAllNonMultiSelectRowsAnimated:notifyDelegate:");
if([self.searchResultsTableView respondsToSelector:selector])
{
[self.searchResultsTableView swizzleMethod:selector withReplacement:JGMethodReplacementProviderBlock {
return JGMethodReplacement(void, UITableView *, BOOL animated, BOOL notifyDelegate) {
};
}];
}
}
[super navigationControllerWillShowViewController:navigationController];
if(isRegularHorizontalClass)
{
[self.searchResultsTableView deswizzle];
}
}
@end
================================================
FILE: Dash/DHSplitViewController.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHSplitViewController : UISplitViewController
@end
================================================
FILE: Dash/DHSplitViewController.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHSplitViewController.h"
#import "DHWebViewController.h"
#import "DHRepo.h"
#import "DHPreferences.h"
#import "DHTocBrowser.h"
#import "DHDocsetBrowser.h"
@implementation DHSplitViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = (id)self;
self.preferredPrimaryColumnWidthFraction = (iPad) ? 0.39 : 0.35;
self.maximumPrimaryColumnWidth = 320;
}
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UINavigationController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController
{
DHWebViewController *webViewController = [DHWebViewController sharedWebViewController];
DHTocBrowser *tocBrowser = webViewController.actualTOCBrowser;
[tocBrowser dismissModal:self];
[[NSNotificationCenter defaultCenter] postNotificationName:DHSplitViewControllerDidCollapse object:nil];
// if([secondaryViewController.topViewController isKindOfClass:[DHRepo class]])
// {
// [secondaryViewController setViewControllers:[NSArray arrayWithObject:secondaryViewController.topViewController] animated:NO];
// return NO;
// }
return YES;
}
- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UINavigationController *)masterViewController
{
[splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAllVisible];
DHWebViewController *webViewController = [DHWebViewController sharedWebViewController];
[webViewController removeDashClearedClass];
DHTocBrowser *tocBrowser = webViewController.actualTOCBrowser;
[tocBrowser dismissModal:self];
NSMutableArray *detailViewControllers = [NSMutableArray arrayWithObject:webViewController];
NSMutableArray *newMasterViewControllers = [NSMutableArray array];
for(UIViewController *viewController in masterViewController.viewControllers)
{
if([viewController isKindOfClass:[DHRepo class]])
{
[detailViewControllers addObject:viewController];
}
else if(![viewController isKindOfClass:[DHWebViewController class]])
{
[newMasterViewControllers addObject:viewController];
}
}
[masterViewController setViewControllers:newMasterViewControllers animated:NO];
UINavigationController *detailNavigationController = [[UINavigationController alloc] initWithRootViewController:webViewController];
detailNavigationController.delegate = webViewController;
if([[masterViewController topViewController] isKindOfClass:[DHPreferences class]] && ![[detailViewControllers lastObject] isKindOfClass:[DHRepo class]])
{
DHPreferences *preferences = (id)[masterViewController topViewController];
NSIndexPath *indexPath = (preferences.tableView.indexPathForSelectedRow) ? preferences.tableView.indexPathForSelectedRow : [NSIndexPath indexPathForRow:0 inSection:0];
NSString *identifier = [[preferences segueIdentifierForIndexPath:indexPath] substringToString:@"ToDetailSegue"];
id viewController = [[DHAppDelegate mainStoryboard] instantiateViewControllerWithIdentifier:identifier];
[detailViewControllers addObject:viewController];
}
else if(![[masterViewController topViewController] isKindOfClass:[DHPreferences class]] && [[detailViewControllers lastObject] isKindOfClass:[DHRepo class]])
{
while([[detailViewControllers lastObject] isKindOfClass:[DHRepo class]])
{
[detailViewControllers removeLastObject];
}
}
[detailNavigationController setViewControllers:detailViewControllers animated:NO];
[masterViewController setToolbarHidden:YES animated:NO];
[[NSNotificationCenter defaultCenter] postNotificationName:DHSplitViewControllerDidSeparate object:nil];
return detailNavigationController;
}
- (NSArray *)keyCommands
{
if([self currentSearchBar])
{
return @[[UIKeyCommand keyCommandWithInput:@"f" modifierFlags:UIKeyModifierCommand action:@selector(handleCommandF) discoverabilityTitle:([self currentSearchBar].placeholder) ? [self currentSearchBar].placeholder : @"Search"]];
}
return @[];
}
- (UISearchBar *)currentSearchBar
{
for(id childController in [self childViewControllers])
{
id controller = childController;
if([childController isKindOfClass:[UINavigationController class]])
{
controller = [childController visibleViewController];
}
if([controller respondsToSelector:@selector(searchBar)] && [controller searchBar])
{
return [controller searchBar];
}
if([controller respondsToSelector:@selector(searchController)] && [controller searchController])
{
return [[[(DHDocsetBrowser*)controller searchController] displayController] searchBar];
}
}
return nil;
}
- (void)handleCommandF
{
[[self currentSearchBar] becomeFirstResponder];
}
@end
================================================
FILE: Dash/DHTarixIndex.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHTarixIndex : NSObject
+ (NSString *)hashForFile:(NSString *)path;
@end
================================================
FILE: Dash/DHTarixIndex.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHTarixIndex.h"
#import "FMDatabase.h"
@implementation DHTarixIndex
+ (NSString *)hashForFile:(NSString *)path
{
NSString *hash = nil;
NSString *docsetPath = [[path substringToStringReturningNil:@".docset"] stringByAppendingString:@".docset"];
NSString *filePath = [[docsetPath lastPathComponent] stringByAppendingPathComponent:[path substringFromStringReturningNil:@".docset"]];
if(filePath.length && docsetPath.length)
{
NSString *indexPath = [docsetPath stringByAppendingPathComponent:@"Contents/Resources/tarixIndex.db"];
NSFileManager *fileManager = [NSFileManager defaultManager];
if([fileManager fileExistsAtPath:indexPath])
{
@synchronized([DHTarixIndex class])
{
FMDatabase *db = [FMDatabase databaseWithPath:indexPath];
if([db openWithFlags:SQLITE_OPEN_READONLY])
{
FMResultSet *rs = [db executeQuery:@"SELECT hash FROM tarindex WHERE path = ?", filePath];
if([rs next])
{
hash = [rs stringForColumnIndex:0];
if(!hash.length)
{
hash = nil;
}
}
[db close];
}
}
}
}
return hash;
}
@end
================================================
FILE: Dash/DHTarixProtocol.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHTarixProtocol : NSURLProtocol
+ (NSMutableData *)alterData:(NSMutableData *)data scheme:(NSString *)scheme path:(NSString *)path extension:(NSString *)extension mimeType:(NSString *)mimeType isTimeout:(BOOL)isTimeout;
@end
================================================
FILE: Dash/DHTarixProtocol.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHTarixProtocol.h"
#import "DHUnarchiver.h"
#import "DHCSS.h"
#import "DHWebViewController.h"
#import "DHWebView.h"
@implementation DHTarixProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if([[[request URL] scheme] isCaseInsensitiveEqual:@"file"])
{
return YES;
}
else if([[[request URL] scheme] isCaseInsensitiveEqual:@"dash-tarix"])
{
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (void)startLoading
{
NSString *path = [[[self.request URL] path] stringByReplacingPercentEscapes];
NSString *extension = [path pathExtension];
NSString *mimeType = [NSString mimeTypeForPathExtension:extension];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[self.request URL]
MIMEType:mimeType
expectedContentLength:-1
textEncodingName:nil];
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
NSMutableData *data = [DHUnarchiver tarixReadFile:path toFile:nil];
if(!data)
{
data = [NSMutableData dataWithContentsOfFile:path];
}
data = [DHTarixProtocol alterData:data scheme:[[self.request URL] scheme] path:path extension:extension mimeType:mimeType isTimeout:NO];
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
}
+ (NSMutableData *)alterData:(NSMutableData *)data scheme:(NSString *)scheme path:(NSString *)path extension:(NSString *)extension mimeType:(NSString *)mimeType isTimeout:(BOOL)isTimeout
{
if(!data)
{
if([[path lastPathComponent] hasPrefix:@"Dash-Intercept-"])
{
path = [[NSBundle mainBundle] pathForResource:[[[path lastPathComponent] stringByDeletingPathExtension] substringFromDashIndex:@"Dash-Intercept-".length] ofType:[path pathExtension]];
data = [NSMutableData dataWithContentsOfFile:path];
}
}
BOOL isHTML = [extension hasCaseInsensitivePrefix:@"htm"] || [mimeType contains:@"html"];
BOOL isCSS = [extension hasCaseInsensitivePrefix:@"css"] || [mimeType contains:@"css"];
BOOL isJS = [extension hasCaseInsensitivePrefix:@"js"];
if(isHTML && data)
{
DHWebViewController *webController = [DHWebViewController sharedWebViewController];
NSString *cssString = [NSString stringWithFormat:@"", [DHCSS currentCSSStringWithTextModifier]];
__block CGRect webViewFrame = CGRectZero;
if([NSThread isMainThread])
{
webViewFrame = webController.webView.frame;
}
else
{
dispatch_sync(dispatch_get_main_queue(), ^{
webViewFrame = webController.webView.frame;
});
}
NSString *viewportString = [NSString stringWithFormat:@"", [DHWebView viewportContent:webViewFrame]];
NSMutableString *content = [[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if(content)
{
[content replaceOccurrencesOfString:@"" withString:cssString options:NSCaseInsensitiveSearch range:NSMakeRange(0, content.length)];
[content replaceOccurrencesOfString:@"" withString:viewportString options:NSCaseInsensitiveSearch range:NSMakeRange(0, content.length)];
data = (id)[content dataUsingEncoding:NSUTF8StringEncoding];
}
}
else if(isCSS && data)
{
NSMutableString *content = [[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if(content)
{
// if I don't remove this, things get weird when I'm scrolling in UIViewC class
// because it believes orientation changes when the toolbar gets hidden
for(NSString *toRemove in @[@"and (orientation:portrait)", @"and (orientation:landscape)"])
{
[content replaceOccurrencesOfString:toRemove withString:@"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, content.length)];
}
data = (id)[content dataUsingEncoding:NSUTF8StringEncoding];
}
}
else if(isJS && data && [path contains:@"Unity 3D.docset"])
{
// Use localStorage because cookie storage doesn't work because UIWebView can't handle the file:// scheme with a
// custom protocol so I'm forced to use the dash-tarix:// scheme, which causes UIWebView to ignore cookies because
// dash-tarix:// URLs aren't really valid. I can't make dash-tarix:// URLs to be valid because I need them to work
// like file:// URLs do (i.e. take advantage of relative paths)
NSMutableString *content = [[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if(content)
{
[content replaceOccurrencesOfString:@"localstorageSupport(){return false;" withString:@"localstorageSupport(){" options:NSCaseInsensitiveSearch range:NSMakeRange(0, content.length)];
data = (id)[content dataUsingEncoding:NSUTF8StringEncoding];
}
}
if(!data && isHTML)
{
data = (id)[[NSString stringWithFormat:@"%@%@
", (isTimeout) ? @"Error Loading" : @"Error Loading", (isTimeout) ? @"Request timed out." : @"Page not found."] dataUsingEncoding:NSUTF8StringEncoding];
}
return data;
}
- (void)stopLoading
{
}
@end
================================================
FILE: Dash/DHTocBrowser.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHWebViewController.h"
@interface DHTocBrowser : UITableViewController
@property (strong) NSMutableArray *sections;
@property (strong) NSMutableArray *sectionTitles;
@property (strong) NSMutableArray *filteredSections;
@property (strong) NSMutableArray *filteredSectionTitles;
@property (weak) UISearchDisplayController *searchController;
@property (assign) IBOutlet UISearchBar *searchBar;
- (IBAction)dismissModal:(id)sender;
@end
================================================
FILE: Dash/DHTocBrowser.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHTocBrowser.h"
#import "DHBrowserTableViewCell.h"
#import "DHJavaScript.h"
#define DHHeaderSeparatorInset 14
@implementation DHTocBrowser
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareForURLSearch:) name:DHPrepareForURLSearch object:nil];
self.clearsSelectionOnViewWillAppear = NO;
if(iPad && isRegularHorizontalClass)
{
self.edgesForExtendedLayout = UIRectEdgeNone;
}
[self.tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
self.tableView.separatorInset = UIEdgeInsetsMake(0, DHHeaderSeparatorInset, 0, 0);
self.tableView.rowHeight = 44;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
[self.tableView reloadData];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.activeSections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger count = [self.activeSections[section] count];
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DHBrowserTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DHBrowserCell" forIndexPath:indexPath];
@try {
NSDictionary *entry = self.activeSections[indexPath.section][indexPath.row];
cell.textLabel.font = [UIFont fontWithName:@"Menlo" size:16];
cell.textLabel.text = entry[@"name"];
cell.imageView.image = [UIImage imageNamed:entry[@"entryType"]];;
cell.accessoryType = (!iPad || !isRegularHorizontalClass) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
[self highlightCell:cell];
}
@catch(NSException *exception) { NSLog(@"%@ %@", exception, [exception callStackSymbols]); }
return cell;
}
- (void)highlightCell:(DHBrowserTableViewCell *)cell
{
if(!self.searchController.active)
{
return;
}
NSRange range;
NSInteger offset = 0;
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:cell.titleLabel.attributedText];
for(NSString *key in [DHDBResult highlightDictionary])
{
[string removeAttribute:key range:NSMakeRange(0, string.length)];
}
NSString *substring = [[string string] copy];
BOOL didAddAttributes = NO;
while((range = [substring rangeOfString:self.searchController.searchBar.text options:NSCaseInsensitiveSearch]).location != NSNotFound)
{
[string addAttributes:[DHDBResult highlightDictionary] range:NSMakeRange(range.location+offset, range.length)];
substring = [substring substringFromDashIndex:range.location+range.length];
offset += range.location+range.length;
didAddAttributes = YES;
}
if(didAddAttributes)
{
cell.titleLabel.attributedText = string;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *entry = self.activeSections[indexPath.section][indexPath.row];
NSString *hash = [entry[@"path"] stringByReplacingPercentEscapes];
DHWebViewController *webViewController = [DHWebViewController sharedWebViewController];
webViewController.nextAnchorChangeNotCausedByUserNavigation = YES;
webViewController.anchorChangeInProgress = YES;
webViewController.ignoreScroll = YES;
[webViewController.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"if(window.location.hash == \"#%@\") { window.location.hash = ''; } window.location.hash = \"#%@\"", hash, hash]];
webViewController.anchorChangeInProgress = NO;
webViewController.ignoreScroll = NO;
if([DHRemoteServer sharedServer].connectedRemote)
{
[[DHRemoteServer sharedServer] sendWebViewURL:[webViewController.webView stringByEvaluatingJavaScriptFromString:@"window.location.href"]];
}
if(!iPad || !isRegularHorizontalClass)
{
[self performSelector:@selector(dismissModal:) withObject:self afterDelay:0.1f];
}
else
{
// if(self.webViewController.navigationController.toolbarHidden)
// {
// self.webViewController.ignoreScroll = YES;
// [self.webViewController.navigationController setToolbarHidden:NO animated:YES];
// self.webViewController.ignoreScroll = NO;
// }
}
}
- (NSMutableArray *)activeSections
{
return self.searchController. active && self.searchController.searchBar.text.length ? self.filteredSections : self.sections;
}
- (NSMutableArray *)activeSectionTitles
{
return self.searchController.active && self.searchController.searchBar.text.length ? self.filteredSectionTitles : self.sectionTitles;
}
- (IBAction)dismissModal:(id)sender
{
[[DHWebViewController sharedWebViewController] removeDashClearedClass];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView
{
self.searchController = controller;
if(isIOS11)
{
if(@available(iOS 11.0, *))
{
tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
tableView.rowHeight = 44;
tableView.separatorInset = UIEdgeInsetsMake(0, DHHeaderSeparatorInset, 0, 0);
if(iPad && isRegularHorizontalClass)
{
tableView.contentInset = UIEdgeInsetsMake(0.0f, 0.f, 0.f, 0.f);
}
[tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
self.filteredSections = [NSMutableArray array];
self.filteredSectionTitles = [NSMutableArray array];
int i = 0;
for(NSArray *section in self.sections)
{
NSMutableArray *filteredSection = [NSMutableArray array];
for(NSDictionary *entry in section)
{
if([entry[@"name"] contains:searchString])
{
[filteredSection addObject:entry];
}
}
if(filteredSection.count)
{
[self.filteredSections addObject:filteredSection];
[self.filteredSectionTitles addObject:self.sectionTitles[i]];
}
++i;
}
return YES;
}
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller
{
[self.tableView deselectAll:NO];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 22;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.activeSectionTitles[section];
}
- (void)prepareForURLSearch:(id)sender
{
if(!iPad || !isRegularHorizontalClass)
{
[self.searchDisplayController setActive:NO animated:NO];
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil];
}
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
================================================
FILE: Dash/DHTransferFeed.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHFeed.h"
#import "DHDocset.h"
@interface DHTransferFeed : DHFeed
@property (strong) DHDocset *docset;
+ (DHTransferFeed *)feedWithPath:(NSString *)path isInstalled:(BOOL)installed;
- (BOOL)isProperlyInstalled;
- (void)cancelInstall;
- (BOOL)loadDocset;
- (BOOL)refreshIcon;
@end
================================================
FILE: Dash/DHTransferFeed.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHTransferFeed.h"
#import "DHDocsetTransferrer.h"
@implementation DHTransferFeed
+ (DHTransferFeed *)feedWithPath:(NSString *)path isInstalled:(BOOL)installed
{
DHTransferFeed *feed = [[self alloc] init];
feed.feedURL = path;
feed.feed = [path lastPathComponent];
feed.installed = installed;
return feed;
}
- (BOOL)loadDocset
{
self.docset = (self.docset) ? : [DHDocset docsetAtPath:self.feedURL];
return self.docset != nil;
}
- (NSString *)docsetNameWithVersion:(BOOL)withVersion
{
return (self.docset) ? self.docset.name : self.feed;
}
- (NSString *)sortName
{
return [self docsetNameWithVersion:NO];
}
- (BOOL)isEqual:(DHFeed *)object
{
return [self.feed isEqualToString:[object feed]];
}
- (NSString *)uniqueIdentifier // Used to find a corresponding feed from a installed docset
{
return self.feed;
}
- (UIImage *)icon
{
return (self.docset) ? self.docset.icon : [UIImage imageNamed:@"Other"];
}
- (void)cancelInstall
{
self.installed = NO;
self.installing = NO;
self.feedResult = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:[transfersPath stringByAppendingPathComponent:self.feed]])
{
[fileManager moveItemAtPath:self.feedURL toPath:[transfersPath stringByAppendingPathComponent:self.feed] error:nil];
self.feedURL = [transfersPath stringByAppendingPathComponent:self.feed];
self.docset = nil;
[self loadDocset];
}
else
{
NSString *trashPath = [[DHDocsetTransferrer sharedTransferrer] uniqueTrashPath];
[fileManager moveItemAtPath:self.feedURL toPath:trashPath error:nil];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[[NSFileManager defaultManager] removeItemAtPath:trashPath error:nil];
});
}
}
- (BOOL)isProperlyInstalled
{
return [[NSFileManager defaultManager] fileExistsAtPath:[self.feedURL stringByAppendingPathComponent:@"Contents/Resources/optimisedIndex.dsidx"]];
}
- (BOOL)refreshIcon
{
if(![self.docset.hasCustomIcon boolValue])
{
self.docset.hasCustomIcon = nil;
[self.docset grabIcon];
return [self.docset.hasCustomIcon boolValue];
}
return NO;
}
@end
================================================
FILE: Dash/DHType.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHType : NSObject {
NSString *humanType;
NSString *humanTypePlural;
NSArray *aliases;
}
@property (retain) NSString *humanType;
@property (retain) NSString *humanTypePlural;
@property (retain) NSArray *aliases;
+ (DHType *)typeWithHumanType:(NSString *)aHumanType humanPlural:(NSString *)aHumanTypePlural aliases:(id)someAliases;
+ (DHType *)typeWithHumanType:(NSString *)aHumanType humanPlural:(NSString *)aHumanTypePlural;
@end
================================================
FILE: Dash/DHType.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHType.h"
@implementation DHType
@synthesize humanType;
@synthesize humanTypePlural;
@synthesize aliases;
+ (DHType *)typeWithHumanType:(NSString *)aHumanType humanPlural:(NSString *)aHumanTypePlural aliases:(id)someAliases
{
DHType *type = [[DHType alloc] initWithHumanType:aHumanType humanPlural:aHumanTypePlural];
[type setAliases:([someAliases isKindOfClass:[NSArray class]]) ? someAliases : @[someAliases]];
return type;
}
+ (DHType *)typeWithHumanType:(NSString *)aHumanType humanPlural:(NSString *)aHumanTypePlural
{
return [[DHType alloc] initWithHumanType:aHumanType humanPlural:aHumanTypePlural];
}
- (id)initWithHumanType:(NSString *)aHumanType humanPlural:(NSString *)aHumanTypePlural
{
self = [super init];
if(self)
{
self.humanType = aHumanType;
self.humanTypePlural = aHumanTypePlural;
}
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@ - %@ - %@", humanType, humanTypePlural, aliases];
}
@end
================================================
FILE: Dash/DHTypeBrowser.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHDocsetBrowser.h"
#import "DHDBSearchController.h"
@interface DHTypeBrowser : UITableViewController
@property (strong) DHDocset *docset;
@property (strong) NSMutableArray *types;
@property (assign) BOOL isLoading;
@property (assign) BOOL isEmpty;
@property (strong) DHDBSearchController *searchController;
@property (assign) BOOL didFirstLoad;
@property (assign) BOOL isRestoring;
@property (assign) BOOL didLoad;
@end
================================================
FILE: Dash/DHTypeBrowser.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHTypeBrowser.h"
#import "DHTypes.h"
#import "DHDocsetManager.h"
@implementation DHTypeBrowser
- (void)viewDidLoad
{
if(!self.docset)
{
// happens during state restoration
return;
}
[super viewDidLoad];
self.clearsSelectionOnViewWillAppear = NO;
self.searchController = [DHDBSearchController searchControllerWithDocsets:@[self.docset] typeLimit:nil viewController:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareForURLSearch:) name:DHPrepareForURLSearch object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(enforceSmartTitleBarButton) name:DHSplitViewControllerDidSeparate object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(enforceSmartTitleBarButton) name:DHSplitViewControllerDidCollapse object:nil];
[self.tableView registerNib:[UINib nibWithNibName:@"DHBrowserCell" bundle:nil] forCellReuseIdentifier:@"DHBrowserCell"];
[self.tableView registerNib:[UINib nibWithNibName:@"DHLoadingCell" bundle:nil] forCellReuseIdentifier:@"DHLoadingCell"];
self.tableView.rowHeight = 44;
self.title = self.docset.name;
[self enforceSmartTitleBarButton];
if(self.isRestoring)
{
return;
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!self.didLoad && self.docset)
{
self.didLoad = YES;
if(isRegularHorizontalClass && self.isActive)
{
[[DHWebViewController sharedWebViewController] loadURL:[self.docset indexFilePath]];
}
NSString *typesCache = [self.docset.path stringByAppendingPathComponent:@".types.plist"];
NSDictionary *typesCacheDict = [NSDictionary dictionaryWithContentsOfFile:typesCache];
if(([self.docset.path contains:@"Apple_API_Reference"] || [self.docset.platform isEqualToString:@"apple"]) && typesCacheDict && [typesCacheDict[@"language"] integerValue] != [DHAppleActiveLanguage currentLanguage])
{
typesCacheDict = nil;
}
if(typesCacheDict && typesCacheDict[@"types"] && [typesCacheDict[@"types"] isKindOfClass:[NSArray class]] && [typesCacheDict[@"types"] count])
{
self.types = [typesCacheDict[@"types"] mutableCopy];
}
else
{
self.tableView.allowsSelection = NO;
self.isLoading = YES;
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[self.docset executeBlockWithinDocsetDBConnection:^(FMDatabase *db) {
NSMutableArray *types = [NSMutableArray array];
NSMutableDictionary *typesDict = [NSMutableDictionary dictionary];
NSConditionLock *lock = [DHDocset stepLock];
NSString *platform = self.docset.platform;
[lock lockWhenCondition:DHLockAllAllowed];
NSString *query = @"SELECT type, COUNT(rowid) FROM searchIndex GROUP BY type";
if([self.docset.platform isEqualToString:@"apple"])
{
if([DHAppleActiveLanguage currentLanguage] == DHNewActiveAppleLanguageSwift)
{
query = @"SELECT type, COUNT(rowid) FROM searchIndex WHERE path NOT LIKE '%%' AND path NOT LIKE '%%' GROUP BY type";
}
else
{
query = @"SELECT type, COUNT(rowid) FROM searchIndex WHERE path NOT LIKE '%%' GROUP BY type";
}
}
FMResultSet *rs = [db executeQuery:query];
BOOL next = [rs next];
[lock unlock];
while(next)
{
NSString *type = [rs stringForColumnIndex:0];
if(type && type.length)
{
NSInteger count = [rs intForColumnIndex:1];
NSString *pluralName = [DHTypes pluralFromEncoded:type];
if([pluralName isEqualToString:@"Categories"] && ([platform isEqualToString:@"python"] || [platform isEqualToString:@"flask"] || [platform isEqualToString:@"twisted"] || [platform isEqualToString:@"django"] || [platform isEqualToString:@"actionscript"] || [platform isEqualToString:@"nodejs"]))
{
pluralName = @"Modules";
}
typesDict[type] = @{@"type": type, @"count": @(count), @"plural": pluralName};
}
[lock lockWhenCondition:DHLockAllAllowed];
next = [rs next];
[lock unlock];
}
NSMutableArray *typeOrder = [NSMutableArray arrayWithArray:[[DHTypes sharedTypes] orderedTypes]];
[typeOrder removeObject:@"Guide"];
[typeOrder removeObject:@"Section"];
[typeOrder removeObject:@"Sample"];
[typeOrder removeObject:@"File"];
[typeOrder addObject:@"Guide"];
[typeOrder addObject:@"Section"];
[typeOrder addObject:@"Sample"];
[typeOrder addObject:@"File"];
if([platform isEqualToString:@"go"] || [platform isEqualToString:@"godoc"])
{
[typeOrder removeObject:@"Type"];
[typeOrder insertObject:@"Type" atIndex:0];
}
if([platform isEqualToString:@"swift"])
{
[typeOrder removeObject:@"Type"];
[typeOrder insertObject:@"Type" atIndex:0];
}
for(NSString *key in typeOrder)
{
NSDictionary *type = typesDict[key];
if(type)
{
[types addObject:type];
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
self.isLoading = NO;
self.isEmpty = types.count <= 0;
if(!self.isEmpty)
{
self.tableView.allowsSelection = YES;
}
self.types = types;
NSMutableDictionary *newTypesCacheDict = [@{@"types": types} mutableCopy];;
if([self.docset.path contains:@"Apple_API_Reference.docset"] || [self.docset.platform isEqualToString:@"apple"])
{
newTypesCacheDict[@"language"] = @([DHAppleActiveLanguage currentLanguage]);
}
[newTypesCacheDict writeToFile:typesCache atomically:NO];
[self.tableView reloadData];
});
} readOnly:YES lockCondition:DHLockAllAllowed optimisedIndex:YES];
});
}
}
[self.tableView deselectAll:YES];
[self.searchController viewWillAppear];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
[self.searchController traitCollectionDidChange:previousTraitCollection];
[self enforceSmartTitleBarButton];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.searchController viewWillDisappear];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.searchController viewDidDisappear];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.searchController viewDidAppear];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(self.isLoading || self.isEmpty)
{
return 3;
}
NSInteger count = self.types.count;
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DHBrowserTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:(self.isLoading || self.isEmpty) ? @"DHLoadingCell" : @"DHBrowserCell" forIndexPath:indexPath];
if((self.isLoading || self.isEmpty) && indexPath.row == 2)
{
cell.userInteractionEnabled = NO;
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
[paragraph setAlignment:NSTextAlignmentCenter];
UIFont *font = [UIFont boldSystemFontOfSize:20];
cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:(self.isEmpty) ? @"Empty Docset" : @"Loading..." attributes:@{NSParagraphStyleAttributeName : paragraph, NSForegroundColorAttributeName: [UIColor colorWithWhite:0.8 alpha:1], NSFontAttributeName: font}];
}
else if(self.isLoading || self.isEmpty)
{
cell.userInteractionEnabled = NO;
cell.textLabel.text = @"";
}
else
{
cell.userInteractionEnabled = YES;
NSDictionary *type = self.types[indexPath.row];
cell.textLabel.text = type[@"plural"];
[cell.titleLabel setRightDetailText:[type[@"count"] stringValue] adjustMainWidth:YES];
cell.imageView.image = [UIImage imageNamed:type[@"type"]];;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self performSegueWithIdentifier:@"DHEntryBrowserSegue" sender:self];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:@"DHEntryBrowserSegue"])
{
id entryBrowser = [segue destinationViewController];
[entryBrowser setDocset:self.docset];
NSDictionary *selectedType = self.types[self.tableView.indexPathForSelectedRow.row];
[entryBrowser setType:selectedType[@"type"]];
[entryBrowser setTitle:selectedType[@"plural"]];
}
else if([[segue identifier] isEqualToString:@"DHShowIndexPageSegue"])
{
DHWebViewController *webViewController = [segue destinationViewController];
webViewController.urlToLoad = [self.docset indexFilePath];
}
else
{
[self.searchController prepareForSegue:segue sender:sender];
}
}
- (void)prepareForURLSearch:(id)sender
{
[self.searchDisplayController setActive:NO animated:NO];
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[self.searchController encodeRestorableStateWithCoder:coder];
[coder encodeObject:self.docset.relativePath forKey:@"docsetRelativePath"];
[coder encodeObject:self.types forKey:@"types"];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
NSString *docsetRelativePath = [coder decodeObjectForKey:@"docsetRelativePath"];
self.docset = [[DHDocsetManager sharedManager] docsetWithRelativePath:docsetRelativePath];
self.isRestoring = YES;
[self viewDidLoad];
self.isRestoring = NO;
self.types = [coder decodeObjectForKey:@"types"];
[self.searchController decodeRestorableStateWithCoder:coder];
[super decodeRestorableStateWithCoder:coder];
}
- (void)enforceSmartTitleBarButton
{
if(isRegularHorizontalClass)
{
if(self.navigationItem.titleView)
{
self.navigationItem.titleView = nil;
self.title = self.docset.name;
}
}
else if(!self.navigationItem.titleView && ![[self.docset indexFilePath] isCaseInsensitiveEqual:[[NSBundle mainBundle] pathForResource:@"home" ofType:@"html"]] && [DHDocsetBrowser titleBarItemAttributedStringTemplate])
{
@try {
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
NSMutableAttributedString *title = [[DHDocsetBrowser titleBarItemAttributedStringTemplate] mutableCopy];
[title.mutableString setString:[NSString stringWithFormat:@"%@ ", self.docset.name]];
[title addAttributes:@{NSForegroundColorAttributeName: [UIColor lightGrayColor], NSFontAttributeName: [UIFont fontWithName:@"Ionicons" size:20]} range:NSMakeRange(title.mutableString.length-1, 1)];
if(isIOS11)
{
[title addAttributes:@{NSBaselineOffsetAttributeName: @(2)} range:NSMakeRange(0, title.mutableString.length-2)];
}
else
{
[title addAttributes:@{NSBaselineOffsetAttributeName: @(-2)} range:NSMakeRange(title.mutableString.length-1, 1)];
}
[button setAttributedTitle:title forState:UIControlStateNormal];
[button addTarget:self action:@selector(smartTitleBarButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
self.navigationItem.titleView = button;
}
@catch(NSException *exception) { NSLog(@"%@ %@", exception, [exception callStackSymbols]); }
}
}
- (void)smartTitleBarButtonPressed:(id)sender
{
[[DHWebViewController sharedWebViewController] loadURL:[self.docset indexFilePath]];
[self performSegueWithIdentifier:@"DHShowIndexPageSegue" sender:self];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
================================================
FILE: Dash/DHTypes.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHTypes : NSObject {
NSMutableArray *orderedTypeObjects;
NSMutableArray *orderedTypes;
NSMutableDictionary *encodedToSingular;
NSMutableDictionary *encodedToPlural;
NSMutableArray *orderedHeaders;
}
@property (retain) NSMutableArray *orderedTypeObjects;
@property (retain) NSMutableArray *orderedTypes;
@property (retain) NSMutableDictionary *encodedToSingular;
@property (retain) NSMutableDictionary *encodedToPlural;
@property (retain) NSMutableArray *orderedHeaders;
+ (DHTypes *)sharedTypes;
+ (NSString *)singularFromEncoded:(NSString *)encodedType notFoundReturn:(NSString *)notFound;
+ (NSString *)pluralFromEncoded:(NSString *)encodedType;
- (NSString *)typeFromScalaType:(NSString*)scalaType;
- (NSString *)unifiedSQLiteOrder:(BOOL)isDashDocset platform:(NSString *)platform;
@end
================================================
FILE: Dash/DHTypes.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHTypes.h"
#import "DHType.h"
@implementation DHTypes
static DHTypes *_types = nil;
@synthesize orderedTypeObjects;
@synthesize orderedTypes;
@synthesize encodedToSingular;
@synthesize encodedToPlural;
@synthesize orderedHeaders;
+ (DHTypes *)sharedTypes
{
@synchronized([DHTypes class])
{
if(!_types)
{
_types = [[DHTypes alloc] init];
[_types setUp];
}
}
return _types;
}
- (void)setUp
{
self.orderedTypeObjects = [NSMutableArray array];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Snippet" humanPlural:@"Snippets"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Class" humanPlural:@"Classes" aliases:@[@"cl", @"tmplt", @"specialization"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"_Struct" humanPlural:@"_Structs"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Tag" humanPlural:@"Tags"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Trait" humanPlural:@"Traits"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Database" humanPlural:@"Databases"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Protocol" humanPlural:@"Protocols" aliases:@"intf"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Delegate" humanPlural:@"Delegates"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Interface" humanPlural:@"Interfaces"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Template" humanPlural:@"Templates"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Indirection" humanPlural:@"Indirections"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Object" humanPlural:@"Objects"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Schema" humanPlural:@"Schemas"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Category" humanPlural:@"Categories" aliases:@[@"cat", @"Groups", @"Pages"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Collection" humanPlural:@"Collections"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Framework" humanPlural:@"Frameworks"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Module" humanPlural:@"Modules"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Library" humanPlural:@"Libraries"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Namespace" humanPlural:@"Namespaces" aliases:@"ns"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Package" humanPlural:@"Packages"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Exception" humanPlural:@"Exceptions"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Struct" humanPlural:@"Structs" aliases:@[@"Data Structures", @"struct"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Type" humanPlural:@"Types" aliases:@[@"tag", @"tdef", @"Public Types", @"Protected Types", @"Private Types", @"Typedefs", @"Package Types", @"Data Types"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Diagram" humanPlural:@"Diagrams"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Table" humanPlural:@"Tables"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Query" humanPlural:@"Queries"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Component" humanPlural:@"Components"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Constructor" humanPlural:@"Constructors" aliases:@[@"Public Constructors"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Element" humanPlural:@"Elements"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Resource" humanPlural:@"Resources"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Directive" humanPlural:@"Directives"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Extension" humanPlural:@"Extensions"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Plugin" humanPlural:@"Plugins"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Filter" humanPlural:@"Filters"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Service" humanPlural:@"Services"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Provider" humanPlural:@"Providers"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Decorator" humanPlural:@"Decorators"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Method" humanPlural:@"Methods" aliases:@[@"instm", @"intfm", @"clm", @"intfcm", @"Class Methods", @"Instance Methods", @"Public Methods", @"Inherited Methods", @"Private Methods", @"Protected Methods", @"instctr", @"intfctr", @"enumm", @"intfsub", @"enumcm", @"structctr", @"structcm", @"enumctr", @"instsub", @"structsub", @"structm"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Property" humanPlural:@"Properties" aliases:@[@"intfp", @"instp", @"Protected Properties", @"Public Properties", @"Inherited Properties", @"Private Properties", @"structp", @"enump", @"intfdata", @"cldata"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Field" humanPlural:@"Fields" aliases:@[@"Data Fields"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Attribute" humanPlural:@"Attributes" aliases:@[@"XML Attributes", @"Public Attributes", @"Static Public Attributes", @"Protected Attributes", @"Static Protected Attributes", @"Private Attributes", @"Static Private Attributes", @"Package Attributes", @"Static Package Attributes"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Index" humanPlural:@"Indexes"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Mixin" humanPlural:@"Mixins"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Event" humanPlural:@"Events" aliases:@[@"event", @"Public Events", @"Inherited Events", @"Private Events"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Binding" humanPlural:@"Bindings" aliases:@"binding"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Foreign Key" humanPlural:@"Foreign Keys"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"View" humanPlural:@"Views"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Special Form" humanPlural:@"Special Forms"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Record" humanPlural:@"Records"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Report" humanPlural:@"Reports"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Modifier" humanPlural:@"Modifiers"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Shortcut" humanPlural:@"Shortcuts"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Trigger" humanPlural:@"Triggers"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Helper" humanPlural:@"Helpers"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Pipe" humanPlural:@"Pipes"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Relationship" humanPlural:@"Relationships"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Column" humanPlural:@"Columns"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Function" humanPlural:@"Functions" aliases:@[@"func", @"ffunc", @"signal", @"slot", @"dcop", @"Public Member Functions", @"Static Public Member Functions", @"Protected Member Functions", @"Static Protected Member Functions", @"Private Member Functions", @"Static Private Member Functions", @"Package Functions", @"Static Package Functions", @"Functions/Subroutines", @"Function Prototypes", @"Public Slots", @"Signals", @"Protected Slots", @"Private Slots", @"Members", @"grammar"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Expression" humanPlural:@"Expressions"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Hook" humanPlural:@"Hooks"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Procedure" humanPlural:@"Procedures"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Subroutine" humanPlural:@"Subroutines"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Builtin" humanPlural:@"Builtins"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Word" humanPlural:@"Words"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Callback" humanPlural:@"Callbacks"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Handler" humanPlural:@"Handlers"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Control Structure" humanPlural:@"Control Structures"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Annotation" humanPlural:@"Annotations"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"File" humanPlural:@"Files"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Error" humanPlural:@"Errors"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Enum" humanPlural:@"Enums" aliases:@[@"Enumerations", @"enum"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Tactic" humanPlural:@"Tactics"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Environment" humanPlural:@"Environments"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Command" humanPlural:@"Commands"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Provisioner" humanPlural:@"Provisioners"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Axiom" humanPlural:@"Axioms"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Lemma" humanPlural:@"Lemmas"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Inductive" humanPlural:@"Inductives"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Instance" humanPlural:@"Instances"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Global" humanPlural:@"Globals"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Union" humanPlural:@"Unions" aliases:@[@"union"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Variable" humanPlural:@"Variables" aliases:@[@"var", @"Class Variable"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Member" humanPlural:@"Members"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Block" humanPlural:@"Blocks"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Constant" humanPlural:@"Constants" aliases:@[@"clconst", @"econst", @"data", @"Notifications", @"enumelt", @"structdata", @"enumdata", @"writerid"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Macro" humanPlural:@"Macros" aliases:@"macro"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Value" humanPlural:@"Values"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Variant" humanPlural:@"Variants"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Define" humanPlural:@"Defines"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Iterator" humanPlural:@"Iterators"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Literal" humanPlural:@"Literals"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Widget" humanPlural:@"Widgets"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Keyword" humanPlural:@"Keywords"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Instruction" humanPlural:@"Instructions"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Request" humanPlural:@"Requests"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Message" humanPlural:@"Messages"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Option" humanPlural:@"Options"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Setting" humanPlural:@"Settings"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Style" humanPlural:@"Styles"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Script" humanPlural:@"Scripts"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Notation" humanPlural:@"Notations"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Abbreviation" humanPlural:@"Abbreviations"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Projection" humanPlural:@"Projection"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Parameter" humanPlural:@"Parameters"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Syntax" humanPlural:@"Syntaxes"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Signature" humanPlural:@"Signatures"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Conversion" humanPlural:@"Conversions"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Pattern" humanPlural:@"Patterns"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Test" humanPlural:@"Tests"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Operator" humanPlural:@"Operators" aliases:@[@"opfunc"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Statement" humanPlural:@"Statements"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Role" humanPlural:@"Roles"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Register" humanPlural:@"Registers"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"State" humanPlural:@"States"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Alias" humanPlural:@"Aliases"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Device" humanPlural:@"Devices"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Kind" humanPlural:@"Kinds"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Node" humanPlural:@"Nodes"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Flag" humanPlural:@"Flags"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Sender" humanPlural:@"Senders"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Data Source" humanPlural:@"Data Sources"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Reference" humanPlural:@"References"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Guide" humanPlural:@"Guides" aliases:@[@"doc"]]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Sample" humanPlural:@"Samples"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Section" humanPlural:@"Sections"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Entry" humanPlural:@"Entries"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Glossary" humanPlural:@"Glossaries"]];
[orderedTypeObjects addObject:[DHType typeWithHumanType:@"Unknown" humanPlural:@"Unknown"]];
self.orderedTypes = [NSMutableArray array];
self.encodedToSingular = [NSMutableDictionary dictionary];
self.encodedToPlural = [NSMutableDictionary dictionary];
self.orderedHeaders = [NSMutableArray array];
for(DHType *type in orderedTypeObjects)
{
[orderedTypes addObject:type.humanType];
encodedToSingular[type.humanType] = type.humanType;
encodedToSingular[type.humanTypePlural] = type.humanType;
encodedToPlural[type.humanType] = type.humanTypePlural;
[orderedHeaders addObject:type.humanTypePlural];
for(NSString *alias in type.aliases)
{
encodedToSingular[alias] = type.humanType;
encodedToPlural[alias] = type.humanTypePlural;
[orderedHeaders addObject:alias];
}
}
[orderedHeaders addObject:@"See also"];
}
+ (NSString *)singularFromEncoded:(NSString *)encodedType notFoundReturn:(NSString *)notFound
{
NSString *singular = [[DHTypes sharedTypes] encodedToSingular][encodedType];
if(!singular)
{
return notFound;
}
return singular;
}
+ (NSString *)pluralFromEncoded:(NSString *)encodedType
{
if([encodedType isEqualToString:@"_Struct"])
{
return @"Structs";
}
if([encodedType isEqualToString:@"_Type"])
{
return @"Types";
}
NSString *plural = [[DHTypes sharedTypes] encodedToPlural][encodedType];
if(!plural)
{
return @"Unknown";
}
return plural;
}
- (NSString *)typeFromScalaType:(NSString*)scalaType
{
if([scalaType hasSuffix:@"new"])
{
return @"Constructor";
}
else if([scalaType hasSuffix:@"def"])
{
return @"Function";
}
else if([scalaType hasSuffix:@"val"])
{
return @"Constant";
}
else if([scalaType hasSuffix:@"var"])
{
return @"Variable";
}
else if([scalaType hasSuffix:@"trait"])
{
return @"Trait";
}
else if([scalaType hasSuffix:@"class"])
{
return @"Class";
}
else if([scalaType hasSuffix:@"type"])
{
return @"Type";
}
else if([scalaType hasSuffix:@"object"])
{
return @"Object";
}
else if([scalaType hasSuffix:@"package"])
{
return @"Package";
}
return nil;
}
- (NSString *)unifiedSQLiteOrder:(BOOL)isDashDocset platform:(NSString *)platform
{
BOOL isPHP = [platform isEqualToString:@"php"];
BOOL isSwift = [platform isEqualToString:@"swift"];
BOOL isGo = [platform isEqualToString:@"go"] || [platform isEqualToString:@"godoc"];
NSMutableString *query = [NSMutableString stringWithString:@"ORDER BY (CASE "];
NSUInteger count = 1;
NSMutableArray *arrayToUse = (isPHP || isGo || isSwift) ? [NSMutableArray arrayWithArray:self.orderedTypeObjects] : self.orderedTypeObjects;
if(isPHP)
{
for(DHType *type in [NSArray arrayWithArray:arrayToUse])
{
if([type.humanType isEqualToString:@"Function"])
{
[arrayToUse removeObjectIdenticalTo:type];
[arrayToUse insertObject:type atIndex:0];
break;
}
}
}
if(isGo)
{
for(DHType *type in [NSArray arrayWithArray:arrayToUse])
{
if([type.humanType isEqualToString:@"Type"])
{
[arrayToUse removeObjectIdenticalTo:type];
[arrayToUse insertObject:type atIndex:0];
break;
}
}
}
if(isSwift)
{
for(DHType *type in [NSArray arrayWithArray:arrayToUse])
{
if([type.humanType isEqualToString:@"Type"])
{
[arrayToUse removeObjectIdenticalTo:type];
[arrayToUse insertObject:type atIndex:0];
break;
}
}
}
for(DHType *type in arrayToUse)
{
NSMutableArray *toAppend = [NSMutableArray array];
[toAppend addObject:type.humanType];
for(NSString *alias in type.aliases)
{
[toAppend addObject:alias];
}
for(NSString *append in toAppend)
{
[query appendFormat:(isDashDocset) ? @"WHEN type = '%@' THEN %ld " : @"WHEN ty.ZTYPENAME = '%@' THEN %ld ", append, (unsigned long)count];
}
++count;
}
[query appendFormat:@"ELSE %ld END)", (unsigned long)count];
[query appendString:(isDashDocset) ? @", LENGTH(name) ASC;" : @", LENGTH(t.ZTOKENNAME) ASC;"];
return query;
}
@end
================================================
FILE: Dash/DHUnarchiver.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHUnarchiver : NSObject
+ (BOOL)unpackTarixDocset:(NSString *)path tarixPath:(NSString *)tarixPath delegate:(id)delegate;
+ (BOOL)unarchiveArchive:(NSString *)path delegate:(id)delegate;
+ (NSMutableData *)tarixReadFile:(NSString *)file toFile:(NSString *)outputPath;
+ (NSMutableData *)tarixReadArchive:(NSString *)archive blockNum:(unsigned long)blocknum offset:(off_t)offset blockLength:(unsigned long)blocklength toFile:(NSString *)outputPath;
@end
================================================
FILE: Dash/DHUnarchiver.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHUnarchiver.h"
#import "archive.h"
#import "archive_entry.h"
#import "zlib.h"
@implementation DHUnarchiver
+ (BOOL)unpackTarixDocset:(NSString *)archivePath tarixPath:(NSString *)tarixPath delegate:(id)delegate
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *folder = [tarixPath stringByDeletingLastPathComponent];
BOOL success = NO;
FMDatabase *db = [FMDatabase databaseWithPath:tarixPath];
if([db openWithFlags:SQLITE_OPEN_READONLY])
{
FMResultSet *rs = [db executeQuery:@"SELECT * FROM toextract"];
while([rs next])
{
success = YES;
NSString *path = [rs stringForColumnIndex:0];
NSString *hash = [rs stringForColumnIndex:1];
NSString *fullPath = [folder stringByAppendingPathComponent:path];
NSString *blockNumString = [hash substringToString:@" "];
NSInteger blockNum = [blockNumString integerValue];
hash = [hash substringFromString:@" "];
NSString *offsetString = [hash substringToString:@" "];
NSInteger offset = [offsetString integerValue];
hash = [hash substringFromString:@" "];
NSString *blockLengthString = [hash substringToString:@" "];
NSInteger blockLength = [blockLengthString integerValue];
[fileManager createDirectoryAtPath:[fullPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
if(![self tarixReadArchive:archivePath blockNum:blockNum offset:offset blockLength:blockLength toFile:fullPath])
{
success = NO;
break;
}
if([delegate isCancelled])
{
success = NO;
break;
}
}
}
[db close];
return success;
}
+ (BOOL)unarchiveArchive:(NSString *)path delegate:(id)delegate
{
[delegate setRightDetail:@"Extracting..."];
chdir([[path stringByDeletingLastPathComponent] UTF8String]);
if(extract([[path lastPathComponent] UTF8String], delegate) == 0)
{
return YES;
}
return NO;
}
// Only call this *after* the docset has been installed
+ (NSMutableData *)tarixReadFile:(NSString *)file toFile:(NSString *)outputPath
{
NSString *hash = [DHTarixIndex hashForFile:file];
if(hash)
{
NSArray *hashComponents = [hash componentsSeparatedByString:@" "];
if(hash && hashComponents.count == 3)
{
long long blockNum = [hashComponents[0] longLongValue];
long long offset = [hashComponents[1] longLongValue];
long long blockLength = [hashComponents[2] longLongValue];
NSString *archivePath = [[file substringToString:@".docset/Contents/Resources/"] stringByAppendingString:@".docset/Contents/Resources/tarix.tgz"];
return [DHUnarchiver tarixReadArchive:archivePath blockNum:(unsigned long)blockNum offset:offset blockLength:(unsigned long)blockLength toFile:outputPath];
}
}
return nil;
}
// Order in tarix file is 0 (file) or 5 (folder), blocknum, offset, blocklength
+ (NSMutableData *)tarixReadArchive:(NSString *)archive blockNum:(unsigned long)blockNum offset:(off_t)offset blockLength:(unsigned long)blockLength toFile:(NSString *)outputPath
{
NSString *tarPath = [outputPath stringByAppendingString:@".tar"];
NSOutputStream *stream = (tarPath) ? [NSOutputStream outputStreamToFileAtPath:tarPath append:NO] : nil;
[stream open];
NSMutableData *data = [[NSMutableData alloc] init];
int tarFile = open([archive UTF8String], O_RDONLY);
if(tarFile < 0)
{
NSLog(@"Couldn't open tarfile");
return nil;
}
Bytef *inBuffer = nil;
Bytef *outBuffer = nil;
z_streamp zlibStream = calloc(1, sizeof(z_stream));
@try {
if(inflateInit2(zlibStream, -MAX_WBITS) != Z_OK)
{
NSLog(@"Couldn't init zlib stream");
return nil;
}
if(lseek(tarFile, offset, SEEK_SET) < 0)
{
NSLog(@"Couldn't seek tarfile");
return nil;
}
int blockSize = 512;
inBuffer = malloc(blockSize*20);
outBuffer = malloc(blockSize*20);
NSInteger totalSizeRemaining = blockLength*blockSize;
for(NSUInteger i = 0; i < blockLength && totalSizeRemaining > 0; i += 20)
{
zlibStream->next_in = inBuffer;
NSInteger inLength = read(tarFile, inBuffer, (blockLength-i<20) ? blockSize*(blockLength-i) : blockSize*20);
if(inLength < 0)
{
NSLog(@"Error reading tarfile");
return nil;
}
zlibStream->avail_in = (uint)inLength;
while(zlibStream->avail_in > 0 && totalSizeRemaining > 0)
{
zlibStream->next_out = outBuffer;
zlibStream->avail_out = blockSize*20;
int status = inflate(zlibStream, Z_SYNC_FLUSH);
if(status != Z_OK && status != Z_STREAM_END)
{
NSLog(@"Error inflating buffer");
return nil;
}
NSUInteger outLength = blockSize*20-zlibStream->avail_out;
if(outLength > totalSizeRemaining)
{
outLength = totalSizeRemaining;
totalSizeRemaining = 0;
}
totalSizeRemaining -= outLength;
if(!stream)
{
[data appendBytes:outBuffer length:outLength];
}
else
{
[stream write:outBuffer maxLength:outLength];
}
}
}
}
@finally {
free(inBuffer);
free(outBuffer);
deflateEnd(zlibStream);
free(zlibStream);
close(tarFile);
[stream close];
}
if(tarPath)
{
BOOL success = extract_single_file([tarPath UTF8String], outputPath) == 0;
[[NSFileManager defaultManager] removeItemAtPath:tarPath error:nil];
if(!success)
{
[[NSFileManager defaultManager] removeItemAtPath:outputPath error:nil];
}
return (success) ? data : nil;
}
return extract_from_memory([data bytes], data.length);
}
static int copy_data(struct archive *ar, struct archive *aw)
{
int r;
const void *buff;
size_t size;
off_t offset;
for (;;) {
r = tk_archive_read_data_block(ar, &buff, &size, &offset);
if (r == tk_archive_EOF)
return (tk_archive_OK);
if (r < tk_archive_OK)
return (r);
r = (int)tk_archive_write_data_block(aw, buff, size, offset);
if (r < tk_archive_OK) {
fprintf(stderr, "%s\n", tk_archive_error_string(aw));
return (r);
}
}
}
off_t fsize(const char *filename) {
struct stat st;
if (stat(filename, &st) == 0)
return st.st_size;
return -1;
}
NSMutableData * extract_from_memory(const void * buff, size_t size)
{
NSMutableData *data = [[NSMutableData alloc] init];
struct archive *a;
struct tk_archive_entry *entry;
int r;
a = tk_archive_read_new();
tk_archive_read_support_format_tar(a);
tk_archive_read_support_compression_none(a);
if ((r = tk_archive_read_open_memory(a, (void*)buff, size)))
{
data = nil;
goto cleanup;
}
for (;;) {
r = tk_archive_read_next_header(a, &entry);
if (r == tk_archive_EOF)
break;
if (r < tk_archive_OK)
fprintf(stderr, "%s\n", tk_archive_error_string(a));
if (r < tk_archive_WARN)
{
data = nil;
goto cleanup;
}
if (tk_archive_entry_size(entry) > 0) {
const void *readBuff;
size_t readSize;
off_t offset;
for (;;) {
r = tk_archive_read_data_block(a, &readBuff, &readSize, &offset);
if (r == tk_archive_EOF)
break;
if (r < tk_archive_OK)
break;
[data appendBytes:readBuff length:readSize];
}
if (r < tk_archive_WARN)
{
data = nil;
goto cleanup;
}
}
if (r < tk_archive_WARN)
{
data = nil;
goto cleanup;
}
}
cleanup:
tk_archive_read_close(a);
tk_archive_read_finish(a);
return data;
}
static int extract_single_file(const char *filename, NSString *output_file)
{
NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:output_file append:NO];
[stream open];
struct archive *a;
struct tk_archive_entry *entry;
int r = 0;
int ret = 0;
a = tk_archive_read_new();
tk_archive_read_support_format_tar(a);
tk_archive_read_support_compression_none(a);
if ((r = tk_archive_read_open_filename(a, filename, 10240)))
{
ret = 1;
goto cleanup;
}
for (;;) {
r = tk_archive_read_next_header(a, &entry);
if (r == tk_archive_EOF)
break;
if (r < tk_archive_OK)
fprintf(stderr, "%s\n", tk_archive_error_string(a));
if (r < tk_archive_WARN)
{
ret = 1;
goto cleanup;
}
if (tk_archive_entry_size(entry) > 0) {
const void *buff;
size_t size;
off_t offset;
for (;;) {
r = tk_archive_read_data_block(a, &buff, &size, &offset);
if (r == tk_archive_EOF)
break;
if (r < tk_archive_OK)
break;
[stream write:buff maxLength:size];
}
if (r < tk_archive_WARN)
{
ret = 1;
goto cleanup;
}
}
if (r < tk_archive_WARN)
{
ret = 1;
goto cleanup;
}
}
cleanup:
tk_archive_read_close(a);
tk_archive_read_finish(a);
[stream close];
return ret;
}
static int extract(const char *filename, id delegate)
{
struct archive *a;
struct archive *ext;
struct tk_archive_entry *entry;
int flags;
int r;
flags = tk_archive_EXTRACT_TIME;
flags |= tk_archive_EXTRACT_PERM;
flags |= tk_archive_EXTRACT_ACL;
flags |= tk_archive_EXTRACT_FFLAGS;
a = tk_archive_read_new();
tk_archive_read_support_format_all(a);
tk_archive_read_support_compression_all(a);
ext = tk_archive_write_disk_new();
tk_archive_write_disk_set_options(ext, flags);
tk_archive_write_disk_set_standard_lookup(ext);
int ret = 0;
if ((r = tk_archive_read_open_filename(a, filename, 10240)))
{
ret = 1;
goto cleanup;
}
off_t total = fsize(filename);
__LA_INT64_T previousProgress = 0;
double lastPercent = 0;
for (;;) {
if(total > 0)
{
__LA_INT64_T progress = tk_archive_position_compressed(a);
if(previousProgress != progress)
{
double currentPercent = (double)progress/total;
double delta = currentPercent - lastPercent;
if(delta > 0.005f || delta < -0.005f || progress == total)
{
lastPercent = currentPercent;
[delegate setUnarchiveProgress:currentPercent];
}
}
previousProgress = progress;
}
if([delegate isCancelled])
{
goto cleanup;
}
r = tk_archive_read_next_header(a, &entry);
if (r == tk_archive_EOF)
break;
if (r < tk_archive_OK)
fprintf(stderr, "%s\n", tk_archive_error_string(a));
if (r < tk_archive_WARN)
{
ret = 1;
goto cleanup;
}
r = tk_archive_write_header(ext, entry);
if (r < tk_archive_OK)
fprintf(stderr, "%s\n", tk_archive_error_string(ext));
else if (tk_archive_entry_size(entry) > 0) {
r = copy_data(a, ext);
if (r < tk_archive_OK)
fprintf(stderr, "%s\n", tk_archive_error_string(ext));
if (r < tk_archive_WARN)
{
ret = 1;
goto cleanup;
}
}
r = tk_archive_write_finish_entry(ext);
if (r < tk_archive_OK)
fprintf(stderr, "%s\n", tk_archive_error_string(ext));
if (r < tk_archive_WARN)
{
ret = 1;
goto cleanup;
}
}
cleanup:
tk_archive_read_close(a);
tk_archive_read_finish(a);
tk_archive_write_close(ext);
tk_archive_write_finish(ext);
return ret;
}
- (void)setUnarchiveProgress:(double)progress
{
}
- (void)setRightDetail:(NSString *)detail
{
}
- (BOOL)isCancelled
{
return NO;
}
@end
================================================
FILE: Dash/DHUserRepo.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHRepo.h"
@interface DHUserRepo : DHRepo
@property (strong) NSDate *lastListLoad;
+ (instancetype)sharedUserRepo;
@end
================================================
FILE: Dash/DHUserRepo.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHUserRepo.h"
#import "DHUserRepoList.h"
@implementation DHUserRepo
static id singleton = nil;
+ (instancetype)sharedUserRepo
{
if(singleton)
{
return singleton;
}
id userRepo = [[DHAppDelegate mainStoryboard] instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
[userRepo setUp];
return userRepo;
}
- (void)setUp
{
[super setUp];
[self reloadUserDocsetsIfNeeded];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self reloadUserDocsetsIfNeeded];
}
- (void)reloadUserDocsetsIfNeeded
{
if(!self.loading && (!self.lastListLoad || (!self.searchBar.text.length && [[NSDate date] timeIntervalSinceDate:self.lastListLoad] > 300)))
{
self.loading = YES;
BOOL shouldDelay = [self.loadingText contains:@"Retrying"];
self.loadingText = nil;
self.searchBar.userInteractionEnabled = NO;
self.searchBar.alpha = 0.5;
self.searchBar.placeholder = @"Loading...";
[self.tableView reloadData];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
if(shouldDelay)
{
[NSThread sleepForTimeInterval:1.0];
}
[[DHUserRepoList sharedUserRepoList] reload];
NSMutableArray *feeds = [[DHUserRepoList sharedUserRepoList] allUserDocsets];
dispatch_sync(dispatch_get_main_queue(), ^{
if(feeds.count)
{
NSArray *savedFeeds = [[NSUserDefaults standardUserDefaults] objectForKey:[self defaultsKey]];
for(NSDictionary *feedDictionary in savedFeeds)
{
DHFeed *savedFeed = [DHFeed feedWithDictionaryRepresentation:feedDictionary];
NSUInteger index = [feeds indexOfObject:savedFeed];
if(index != NSNotFound)
{
DHFeed *feed = feeds[index];
feed.installed = savedFeed.installed;
feed.installedVersion = savedFeed.installedVersion;
feed.size = savedFeed.size;
}
}
[feeds sortUsingFunction:compareFeeds context:nil];
self.lastListLoad = [NSDate date];
self.searchBar.userInteractionEnabled = YES;
self.searchBar.alpha = 1.0;
self.searchBar.placeholder = @"Find docsets to download";
self.loading = NO;
self.feeds = feeds;
[self.tableView reloadData];
}
else
{
self.loadingText = @"Loading failed. Retrying...";
[self.tableView reloadData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.loading = NO;
[self reloadUserDocsetsIfNeeded];
});
}
});
});
}
}
- (NSString *)installFeed:(DHFeed *)feed isAnUpdate:(BOOL)isAnUpdate
{
NSObject *identifier = [[NSObject alloc] init];
feed.identifier = identifier;
feed.waiting = YES;
BOOL didStallOnce = NO;
BOOL didSetStallLabel = NO;
while([self shouldStall] && feed.installing && feed.identifier == identifier)
{
if(didStallOnce && !didSetStallLabel)
{
didSetStallLabel = YES;
dispatch_sync(dispatch_get_main_queue(), ^{
[feed setDetailString:@"Waiting..."];
[[feed cell].titleLabel setRightDetailText:@"Waiting..." adjustMainWidth:YES];
[feed setMaxRightDetailWidth:[feed cell].titleLabel.maxRightDetailWidth];
});
}
didStallOnce = YES;
[NSThread sleepForTimeInterval:1.0f];
}
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
feed.waiting = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *installPath = [self docsetPathForFeed:feed];
NSString *tempPath = [self uniqueTempDirAtPath:installPath];
NSString *tempFile = [tempPath stringByAppendingPathComponent:@"dash_temp_docset.tgz"];
NSString *tarixFile = [tempFile stringByAppendingString:@".tarix"];
__block BOOL shouldWait = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
shouldWait = [[DHLatencyTester sharedLatency] performTests:NO];
});
if(shouldWait)
{
[NSThread sleepForTimeInterval:3.0f];
}
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
NSString *error = nil;
DHFeedResult *feedResult = [self loadFeed:feed error:&error];
if(!feed.installing || feed.identifier != identifier)
{
return @"cancelled";
}
if(feedResult && feed.installing)
{
feed.feedResult = feedResult;
feedResult.feed = feed;
NSError *downloadError = nil;
if(feedResult.downloadURLs.count)
{
NSString *downloadURL = feedResult.downloadURLs[0];
[self emptyTrashAtPath:tempPath];
if(![fileManager createDirectoryAtPath:tempPath withIntermediateDirectories:YES attributes:nil error:nil])
{
return @"Couldn't create install directory.";
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
NSURL *url = [NSURL URLWithString:downloadURL];
if(url)
{
BOOL result = NO;
NSURL *tarixURL = [NSURL URLWithString:[[downloadURL stringByAppendingString:@".tarix"] stringByConvertingKapeliHttpURLToHttps]];
feedResult.hasTarix = [NSURL URLIsFound:[tarixURL absoluteString] timeoutInterval:120.0f checkForRedirect:YES];
downloadError = nil;
#ifdef DEBUG
NSLog(@"Downloading %@", url);
#endif
if([DHFileDownload downloadItemAtURL:url toFile:tempFile error:&downloadError delegate:self identifier:feedResult] && !downloadError)
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Waiting..."];
@synchronized([DHDocsetIndexer class])
{
if(!feedResult.hasTarix)
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[fileManager removeItemAtPath:tarixFile error:nil];
result = [DHUnarchiver unarchiveArchive:tempFile delegate:feedResult];
}
else
{
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Preparing..."];
result = [DHFileDownload downloadItemAtURL:tarixURL toFile:tarixFile error:nil delegate:nil identifier:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't download index file.";
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
[feedResult setRightDetail:@"Extracting..."];
result = [DHUnarchiver unarchiveArchive:tarixFile delegate:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive index file.";
}
[fileManager removeItemAtPath:tarixFile error:nil];
tarixFile = [fileManager firstFileWithExtension:@"tarix" atPath:tempPath ignoreHidden:YES];
if(!tarixFile)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't find index file.";
}
tarixFile = [tempPath stringByAppendingPathComponent:tarixFile];
result = [DHUnarchiver unpackTarixDocset:tempFile tarixPath:tarixFile delegate:feedResult];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive docset.";
}
}
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
if(!result)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't unarchive docset.";
}
if(!feedResult.hasTarix)
{
[fileManager removeItemAtPath:tempFile error:nil];
}
DHDocset *docset = [DHDocset firstDocsetInsideFolder:tempPath];
if(!docset)
{
[self emptyTrashAtPath:tempPath];
return @"Couldn't install docset.";
}
else if(feedResult.hasTarix)
{
[fileManager moveItemAtPath:tarixFile toPath:docset.tarixIndexPath error:nil];
[fileManager moveItemAtPath:tempFile toPath:docset.tarixPath error:nil];
}
NSString *iconPath = [docset.path stringByAppendingPathComponent:@"icon.png"];
[fileManager removeItemAtPath:iconPath error:nil];
NSString *icon2xPath = [docset.path stringByAppendingPathComponent:@"icon@2x.png"];
[fileManager removeItemAtPath:icon2xPath error:nil];
[fileManager removeItemAtPath:[docset.path stringByAppendingPathComponent:@"icon.tiff"] error:nil];
NSString *base64 = [DHUserRepoList sharedUserRepoList].json[@"docsets"][feed.uniqueIdentifier][@"icon"];
if(base64.length)
{
[[[NSData alloc] initWithBase64EncodedString:base64 options:NSDataBase64DecodingIgnoreUnknownCharacters] writeToFile:iconPath atomically:NO];
}
base64 = [DHUserRepoList sharedUserRepoList].json[@"docsets"][feed.uniqueIdentifier][@"icon@2x"];
if(base64.length)
{
[[[NSData alloc] initWithBase64EncodedString:base64 options:NSDataBase64DecodingIgnoreUnknownCharacters] writeToFile:icon2xPath atomically:NO];
}
NSString *plistPath = [docset.path stringByAppendingPathComponent:@"Contents/Info.plist"];
NSMutableDictionary *plist = [[NSDictionary dictionaryWithContentsOfFile:plistPath] mutableCopy];
NSString *platform = [plist[@"DocSetPlatformFamily"] trimWhitespace];
if(platform && platform.length)
{
plist[@"DashDocSetKeyword"] = (plist[@"DashDocSetKeyword"]) ? plist[@"DashDocSetKeyword"] : platform;
plist[@"DashDocSetPluginKeyword"] = (plist[@"DashDocSetPluginKeyword"]) ? plist[@"DashDocSetPluginKeyword"] : platform;
plist[@"DashWebSearchKeyword"] = (plist[@"DashWebSearchKeyword"]) ? plist[@"DashWebSearchKeyword"] : platform;
plist[@"DocSetPlatformFamily"] = [@"usercontrib" stringByAppendingString:feed.uniqueIdentifier];
[plist writeToFile:plistPath atomically:NO];
}
[DHDocsetIndexer indexerForDocset:docset delegate:feedResult];
[fileManager removeItemAtPath:docset.sqlPath error:nil];
[fileManager removeItemAtPath:[docset.sqlPath stringByAppendingString:@"-shm"] error:nil];
[fileManager removeItemAtPath:[docset.sqlPath stringByAppendingString:@"-wal"] error:nil];
if([feedResult isCancelled])
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:installPath];
NSString *file = nil;
while(file = [dirEnum nextObject])
{
[dirEnum skipDescendents];
NSString *filePath = [installPath stringByAppendingPathComponent:file];
if(![filePath isEqualToString:tempPath])
{
NSString *trashPath = [self uniqueTrashPath];
[fileManager moveItemAtPath:filePath toPath:trashPath error:nil];
dispatch_queue_t queue = dispatch_queue_create([[NSString stringWithFormat:@"%u", arc4random() % 100000] UTF8String], 0);
dispatch_async(queue, ^{
[self emptyTrashAtPath:trashPath];
});
}
}
[fileManager moveItemAtPath:docset.path toPath:[installPath stringByAppendingPathComponent:[docset.path lastPathComponent]] error:nil];
[self emptyTrashAtPath:tempPath];
return nil;
}
}
else if(downloadError.code == DHDownloadCancelled)
{
[self emptyTrashAtPath:tempPath];
return @"cancelled";
}
else
{
[self emptyTrashAtPath:tempPath];
}
}
}
return @"Couldn't download docset.";
}
else
{
return error;
}
return nil;
}
- (DHFeedResult *)loadFeed:(DHFeed *)feed error:(NSString **)returnError
{
DHFeedResult *feedResult = [[DHFeedResult alloc] init];
DHUserRepoList *list = [DHUserRepoList sharedUserRepoList];
feedResult.downloadURLs = @[[list downloadURLForEntry:feed]];
feedResult.version = [list versionForEntry:feed];
return feedResult;
}
- (NSString *)docsetInstallFolderName
{
return @"User Contributed";
}
- (void)showUpdateRequestForFeeds:(NSArray *)toUpdate count:(NSInteger)feedCount docsetList:(NSString *)docsetList
{
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:[self defaultsScheduledUpdateKey]];
[UIAlertView showWithTitle:@"Updates Found" message:[NSString stringWithFormat:@"Updates are available for %ld %@:%@%@", (long)feedCount, (feedCount > 1) ? @"user contributed docsets" : @"user contributed docset", (feedCount > 1) ? @"\n\n" : @" ", docsetList] cancelButtonTitle:@"Maybe Later" otherButtonTitles:@[@"Update"] tapBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
if(buttonIndex != alertView.cancelButtonIndex)
{
if(toUpdate)
{
[self updateFeeds:toUpdate];
}
else
{
[self checkForUpdatesAndShowInterface:NO updateWithoutAsking:YES];
}
}
}];
}
+ (id)alloc
{
if(singleton)
{
return singleton;
}
return [super alloc];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if(singleton)
{
return singleton;
}
self = [super initWithCoder:aDecoder];
singleton = self;
return self;
}
@end
================================================
FILE: Dash/DHUserRepoList.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import "DHFeed.h"
@interface DHUserRepoList : NSObject
@property (retain) NSDictionary *json;
+ (DHUserRepoList *)sharedUserRepoList;
- (NSMutableArray *)allUserDocsets;
- (NSString *)versionForEntry:(DHFeed *)entry;
- (NSString *)downloadURLForEntry:(DHFeed *)entry;
- (void)reload;
- (NSMutableArray *)allVersionsForEntry:(DHFeed *)entry;
- (NSString *)downloadURLForVersionedEntry:(DHFeed *)versionedEntry parentEntry:(DHFeed *)parentEntry;
- (UIImage *)imageForEntry:(DHFeed *)entry;
@end
================================================
FILE: Dash/DHUserRepoList.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHUserRepoList.h"
#import "DHLatencyTester.h"
@implementation DHUserRepoList
+ (DHUserRepoList *)sharedUserRepoList
{
static dispatch_once_t pred;
static DHUserRepoList *_userList = nil;
dispatch_once(&pred, ^{
_userList = [[DHUserRepoList alloc] init];
[_userList setUp];
});
return _userList;
}
- (void)setUp
{
}
- (void)reload
{
BOOL success = NO;
NSString *url = [[[[DHLatencyTester sharedLatency] bestMirror] stringByAppendingString:@"zzz/user_contributed/build/index.json"] stringByConvertingKapeliHttpURLToHttps];
NSString *json = [NSString stringWithContentsOfURLString:url];
if(json)
{
NSDictionary *newJSON = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
if(newJSON)
{
self.json = newJSON;
success = YES;
}
}
if(!success)
{
dispatch_sync(dispatch_get_main_queue(), ^{
[[DHLatencyTester sharedLatency] performTests:YES];
});
}
}
- (NSMutableArray *)allUserDocsets
{
NSMutableArray *entries = [NSMutableArray array];
[self.json[@"docsets"] enumerateKeysAndObjectsUsingBlock:^(id key, NSDictionary *value, BOOL *stop) {
if([value[@"minimum_dash_version"] integerValue] <= 1 && (!value[@"enabled"] || [value[@"enabled"] boolValue]))
{
NSString *platform = [@"usercontrib" stringByAppendingString:key];
DHFeed *entry = [DHFeed entryWithName:value[@"name"] platform:platform icon:nil];
entry.aliases = value[@"aliases"];
entry._isMajorVersioned = [value[@"major_versioned"] boolValue];
entry._uniqueIdentifier = key;
entry.authorLinkText = value[@"author"][@"name"];
entry.authorLinkHref = [NSString stringWithFormat:@"https://github.com/Kapeli/Dash-User-Contributions/tree/master/docsets/%@#readme", entry.uniqueIdentifier];
BOOL hasVersions = [self allVersionsForEntry:entry].count > 0;
entry.doesNotHaveVersions = !hasVersions;
entry._icon = [self imageForEntry:entry];
[entries addObject:entry];
}
}];
if(!entries.count)
{
return nil;
}
return entries;
}
- (UIImage *)imageForEntry:(DHFeed *)entry
{
NSString *base64 = self.json[@"docsets"][entry.uniqueIdentifier][@"icon@2x"];
if(base64.length)
{
return [UIImage imageWithData:[[NSData alloc] initWithBase64EncodedString:base64 options:NSDataBase64DecodingIgnoreUnknownCharacters] scale:2];
}
return nil;
}
- (NSString *)versionForEntry:(DHFeed *)entry
{
NSString *version = self.json[@"docsets"][entry.uniqueIdentifier][@"version"];
if(version)
{
return version;
}
return nil;
}
- (NSMutableArray *)allVersionsForEntry:(DHFeed *)entry
{
NSMutableArray *versions = [NSMutableArray array];
NSArray *jsonVersions = self.json[@"docsets"][entry.uniqueIdentifier][@"specific_versions"];
for(NSDictionary *jsonVersion in jsonVersions)
{
if([jsonVersion[@"archive"] length] && [jsonVersion[@"version"] length])
{
[versions addObject:[jsonVersion[@"version"] substringToString:@"/"]];
}
}
if(!versions.count)
{
return nil;
}
return versions;
}
- (NSString *)downloadURLForEntry:(DHFeed *)entry
{
if(!self.json)
{
return nil;
}
return [[[[DHLatencyTester sharedLatency] bestMirror] stringByAppendingFormat:@"zzz/user_contributed/build/%@/%@", entry.uniqueIdentifier, self.json[@"docsets"][entry.uniqueIdentifier][@"archive"]] stringByConvertingKapeliHttpURLToHttps];
}
- (NSString *)downloadURLForVersionedEntry:(DHFeed *)versionedEntry parentEntry:(DHFeed *)parentEntry
{
for(NSDictionary *specificVersion in self.json[@"docsets"][parentEntry.uniqueIdentifier][@"specific_versions"])
{
if([[specificVersion[@"version"] substringToString:@"/"] isEqualToString:versionedEntry.uniqueIdentifier])
{
return [[[[DHLatencyTester sharedLatency] bestMirror] stringByAppendingFormat:@"zzz/user_contributed/build/%@/%@", parentEntry.uniqueIdentifier, specificVersion[@"archive"]] stringByConvertingKapeliHttpURLToHttps];
}
}
return nil;
}
@end
================================================
FILE: Dash/DHWebProgressView.h
================================================
#import
@interface DHWebProgressView : UIView
@property (nonatomic) float progress;
@property (nonatomic) float actualProgress;
@property (retain) NSTimer *fakeLoadTimer;
@property (nonatomic) UIView *progressBarView;
@property (nonatomic) NSTimeInterval barAnimationDuration; // default 0.1
@property (nonatomic) NSTimeInterval fadeAnimationDuration; // default 0.27
@property (nonatomic) NSTimeInterval fadeOutDelay; // default 0.1
- (void)setProgress:(float)progress animated:(BOOL)animated;
- (void)fakeSetProgress:(float)progress;
@end
================================================
FILE: Dash/DHWebProgressView.m
================================================
//
// The MIT License (MIT)
//
// Copyright (c) 2013 Satoshi Asano
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#import "DHWebProgressView.h"
@implementation DHWebProgressView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self configureViews];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self configureViews];
}
-(void)configureViews
{
self.userInteractionEnabled = NO;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_progressBarView = [[UIView alloc] initWithFrame:CGRectMake(self.bounds.origin.x, self.bounds.origin.y, 0, self.bounds.size.height)];
UIColor *tintColor = [UIColor colorWithRed:22.f / 255.f green:126.f / 255.f blue:251.f / 255.f alpha:1.0]; // iOS7 Safari bar color
if ([UIApplication.sharedApplication.delegate.window respondsToSelector:@selector(setTintColor:)] && UIApplication.sharedApplication.delegate.window.tintColor) {
tintColor = UIApplication.sharedApplication.delegate.window.tintColor;
}
_progressBarView.backgroundColor = tintColor;
[self addSubview:_progressBarView];
_barAnimationDuration = 0.27f;
_fadeAnimationDuration = 0.27f;
_fadeOutDelay = 0.1f;
}
- (void)fakeSetProgress:(float)progress
{
self.fakeLoadTimer = [NSTimer scheduledTimerWithTimeInterval:0.01 block:^{
self.barAnimationDuration = 3.0;
self.fadeAnimationDuration = 3.0;
[self setProgress:progress animated:YES];
self.barAnimationDuration = 0.27;
self.fadeAnimationDuration = 0.27;
} repeats:NO];
}
-(void)setProgress:(float)progress
{
[self setProgress:progress animated:NO];
}
- (void)setProgress:(float)progress animated:(BOOL)animated
{
self.fakeLoadTimer = [self.fakeLoadTimer invalidateTimer];
if(self.actualProgress == progress)
{
return;
}
self.actualProgress = progress;
CGRect currentFrame = [self.progressBarView.layer.presentationLayer frame];
[self.progressBarView.layer removeAllAnimations];
if(!CGRectEqualToRect(currentFrame, CGRectZero))
{
self.progressBarView.frame = currentFrame;
}
BOOL isGrowing = progress > 0.0;
[UIView animateWithDuration:(isGrowing && animated) ? _barAnimationDuration : 0.0 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
CGRect frame = _progressBarView.frame;
frame.size.width = progress * self.bounds.size.width;
_progressBarView.frame = frame;
} completion:nil];
if (progress >= 1.0) {
[UIView animateWithDuration:animated ? _fadeAnimationDuration : 0.0 delay:_fadeOutDelay options:UIViewAnimationOptionCurveEaseInOut animations:^{
_progressBarView.alpha = 0.0;
} completion:^(BOOL completed){
CGRect frame = _progressBarView.frame;
frame.size.width = 0;
_progressBarView.frame = frame;
}];
}
else {
[UIView animateWithDuration:animated ? _fadeAnimationDuration : 0.0 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_progressBarView.alpha = 1.0;
} completion:nil];
}
}
@end
================================================
FILE: Dash/DHWebView.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHWebView : UIWebView
+ (NSString *)viewportContent:(CGRect)frame;
- (void)setHasHistory:(BOOL)hasHistory;
- (void)resetHistory;
- (void)updateViewPortContent:(CGRect)frame;
@end
================================================
FILE: Dash/DHWebView.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHWebView.h"
#import "DHCSS.h"
@implementation DHWebView
- (void)layoutSubviews
{
[super layoutSubviews];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self updateViewPortContent:self.frame];
});
}
- (void)updateViewPortContent:(CGRect)frame
{
[self stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"document.getElementById('dash_viewport').setAttribute('content', '%@');", [DHWebView viewportContent:frame]]];
}
+ (NSString *)viewportContent:(CGRect)frame
{
NSString *content = [NSString stringWithFormat:@"width=%ld", (long)frame.size.width];
return [content stringByAppendingString:@", initial-scale=1"];
}
- (void)setHasHistory:(BOOL)hasHistory
{
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id documentView = [self performSelector:NSSelectorFromString(@"_documentView")];
id webView = [documentView performSelector:NSSelectorFromString(@"webView")];
SEL selector = NSSelectorFromString(@"setMaintainsBackForwardList:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[webView methodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:webView];
[invocation setArgument:&hasHistory atIndex:2];
[invocation invoke];
#pragma clang diagnostic pop
}
@catch(NSException *exception) { NSLog(@"%@ %@", exception, [exception callStackSymbols]); }
}
- (void)resetHistory
{
[self setHasHistory:NO];
[self setHasHistory:YES];
}
@end
================================================
FILE: Dash/DHWebViewController.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
#import
#import "DHDBResult.h"
#import
#import "DHWebProgressView.h"
@class DHTocBrowser;
@interface DHWebViewController : UIViewController
@property (assign) IBOutlet UIWebView *webView;
@property (strong) UIBarButtonItem *backButton;
@property (strong) UIBarButtonItem *forwardButton;
//@property (strong) UIBarButtonItem *stopButton;
//@property (strong) UIBarButtonItem *reloadButton;
@property (strong) UIBarButtonItem *zoomOutButton;
@property (strong) UIBarButtonItem *zoomInButton;
@property (strong) DHWebProgressView *progressView;
@property (strong) DHDBResult *result;
@property (assign) BOOL ignoreScroll;
@property (strong) NSString *mainFrameURL;
@property (strong) NSString *previousMainFrameURL;
@property (strong) NSArray *currentMethods;
@property (strong) UIPopoverController *methodsPopover;
@property (assign) BOOL anchorChangeInProgress;
@property (assign) BOOL visible;
@property (strong) id lastTocBrowser;
@property (weak) UIBarButtonItem *toggleSplitViewButton;
@property (assign) BOOL isRestoring;
@property (assign) BOOL isDecoding;
@property (strong) NSDate *lastLoadDate;
@property (assign) BOOL stopButtonIsShown;
@property (assign) BOOL nextAnchorChangeNotCausedByUserNavigation;
@property (assign) BOOL didLoadInitialRequest;
@property (assign) BOOL didLoadOnce;
@property (strong) NSString *urlToLoad;
@property (assign) BOOL isRestoreScroll;
@property (assign) CGPoint webViewOffset;
+ (instancetype)sharedWebViewController;
- (void)loadURL:(NSString *)urlString;
- (void)loadResult:(DHDBResult *)result;
- (BOOL)dismissMethodsPopoverIfVisible:(BOOL)animated;
- (BOOL)isLocalURL;
- (void)webViewDidChangeLocationWithinPage;
- (NSString *)loadedURL;
- (DHTocBrowser *)actualTOCBrowser;
- (void)updateBackForwardButtonState;
- (void)removeDashClearedClass;
- (void)snippetUseButtonPressed:(id)sender;
- (IBAction)tocButtonPressed:(id)sender;
- (void)reload;
@end
================================================
FILE: Dash/DHWebViewController.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHWebViewController.h"
#import "DHWebViewController.h"
#import "DHNavigationAnimator.h"
#import "JGMethodSwizzler.h"
#import "DHDocsetManager.h"
#import "DHJavaScript.h"
#import "DHCSS.h"
#import "DHAppDelegate.h"
#import "DHUnarchiver.h"
#import "DHTocBrowser.h"
#import "DHJavaScriptBridge.h"
#import "DHRemoteProtocol.h"
#import "DHTypeBrowser.h"
#import "DHEntryBrowser.h"
#import "DHWebView.h"
@implementation DHWebViewController
static id singleton = nil;
- (void)viewDidLoad
{
if([self callStackIsRestoring] && !self.isRestoring)
{
return;
}
[super viewDidLoad];
if(self.didLoadOnce)
{
return;
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareForURLSearch:) name:DHPrepareForURLSearch object:nil];
self.title = @"";
self.webView.scrollView.delegate = self;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.webView.allowsInlineMediaPlayback = YES;
self.webView.delegate = self;
self.webView.dataDetectorTypes = UIDataDetectorTypeNone;
CGFloat progressBarHeight = 2.f;
CGRect navigationBarBounds = self.navigationController.navigationBar.bounds;
CGRect barFrame = CGRectMake(0, navigationBarBounds.size.height - progressBarHeight, navigationBarBounds.size.width, progressBarHeight);
self.progressView = [[DHWebProgressView alloc] initWithFrame:barFrame];
[self.progressView setProgress:0.0f];
self.progressView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAllVisible];
self.splitViewController.presentsWithGesture = NO;
if(isRegularHorizontalClass)
{
self.navigationController.delegate = self;
}
self.backButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"back"] style:UIBarButtonItemStylePlain target:self action:@selector(goBack)];
self.forwardButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"forward"] style:UIBarButtonItemStylePlain target:self action:@selector(goForward)];
self.zoomOutButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"zoomOut"] style:UIBarButtonItemStylePlain target:self action:@selector(zoomOut)];
self.zoomInButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"zoomIn"] style:UIBarButtonItemStylePlain target:self action:@selector(zoomIn)];
// self.stopButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop target:self.webView action:@selector(stopLoading)];
// self.reloadButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reload)];
[self updateBackForwardButtonState];
self.toolbarItems = @[self.backButton, UIBarButtonWithFixedWidth(10), self.forwardButton, [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], self.zoomOutButton, UIBarButtonWithFixedWidth(3), self.zoomInButton];
[self updateStopReloadButtonState];
self.didLoadOnce = YES;
}
- (void)goBack
{
self.ignoreScroll = YES;
[self.webView goBack];
self.ignoreScroll = NO;
[self performSelector:@selector(updateBackForwardButtonState) withObject:self afterDelay:0.01];
}
- (void)goForward
{
self.ignoreScroll = YES;
[self.webView goForward];
self.ignoreScroll = NO;
[self performSelector:@selector(updateBackForwardButtonState) withObject:self afterDelay:0.01];
}
- (void)zoomIn
{
[[DHCSS sharedCSS] modifyTextSize:YES];
[self refreshTextSize];
}
- (void)zoomOut
{
[[DHCSS sharedCSS] modifyTextSize:NO];
[self refreshTextSize];
}
- (void)refreshTextSize
{
if(!isRegularHorizontalClass)
{
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] zoomScriptWithFrame:self.webView.frame]];
}
NSString *js = [[NSString alloc] initWithFormat:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '%@'", [[DHCSS sharedCSS] textSizeAdjust]];
[self.webView stringByEvaluatingJavaScriptFromString:js];
}
- (void)loadResult:(DHDBResult *)result
{
NSString *webViewURL = result.webViewURL;
if(webViewURL)
{
[self loadURL:webViewURL];
}
}
- (void)viewWillAppear:(BOOL)animated
{
if(!self.didLoadInitialRequest && !self.isRestoring)
{
if(self.result)
{
[self loadResult:self.result];
if([DHRemoteServer sharedServer].connectedRemote)
{
[[DHRemoteServer sharedServer] processRemoteTableOfContents];
}
}
else if(self.urlToLoad)
{
[self loadURL:self.urlToLoad];
self.urlToLoad = nil;
}
else
{
id masterViewController = [(UINavigationController*)[self.splitViewController.viewControllers firstObject] topViewController];
if([masterViewController isKindOfClass:[DHTypeBrowser class]] || [masterViewController isKindOfClass:[DHEntryBrowser class]])
{
[self loadURL:[[masterViewController docset] indexFilePath]];
}
else
{
[self loadURL:[[NSBundle mainBundle] pathForResource:@"home" ofType:@"html"]];
}
}
self.didLoadInitialRequest = YES;
}
self.jsContext[@"window"][@"dash"] = [DHJavaScriptBridge sharedBridge];
[super viewWillAppear:animated];
self.ignoreScroll = YES;
if(![DHRemoteServer sharedServer].connectedRemote)
{
self.navigationController.toolbarHidden = NO;
}
CGFloat progressBarHeight = 2.f;
CGRect navigationBarBounds = self.navigationController.navigationBar.bounds;
CGRect barFrame = CGRectMake(0, navigationBarBounds.size.height - progressBarHeight, navigationBarBounds.size.width, progressBarHeight);
[self.navigationController.navigationBar addSubview:self.progressView];
[self.progressView setFrame:barFrame];
[self traitCollectionDidChange:nil];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
if(previousTraitCollection)
{
[super traitCollectionDidChange:previousTraitCollection];
}
if(isRegularHorizontalClass)
{
UIBarButtonItem *expand = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"expand"] style:UIBarButtonItemStylePlain target:self action:@selector(toggleSplitView:)];
self.toggleSplitViewButton = expand;
self.navigationItem.leftBarButtonItems = @[expand];
}
else
{
self.navigationItem.leftBarButtonItems = nil;
}
self.toolbarItems = @[self.backButton, UIBarButtonWithFixedWidth(10), self.forwardButton, [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil], self.zoomOutButton, UIBarButtonWithFixedWidth(3), self.zoomInButton];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.navigationController.toolbar setHidden:YES];
[self.navigationController setToolbarHidden:YES];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.jsContext[@"window"][@"dash"] = nil;
self.ignoreScroll = YES;
[self.progressView removeFromSuperview];
if(!isRegularHorizontalClass)
{
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"document.body.className = document.body.className+' dash_cleared'; document.title = \"%@\";", self.title]];
self.title = @"";
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.ignoreScroll = NO;
if(self.isDecoding)
{
[self.webView reload];
self.isDecoding = NO;
}
}
- (void)webViewDidStartLoad:(UIWebView *)webView
{
// self.title = @"Loading...";
[self updateBackForwardButtonState];
[self.progressView setProgress:0 animated:NO];
[self.progressView fakeSetProgress:0.6];
}
- (void)webViewDidChangeLocationWithinPage
{
if(!self.nextAnchorChangeNotCausedByUserNavigation && [DHRemoteServer sharedServer].connectedRemote)
{
[[DHRemoteServer sharedServer] sendWebViewURL:[self.webView stringByEvaluatingJavaScriptFromString:@"window.location.href"]];
}
self.nextAnchorChangeNotCausedByUserNavigation = NO;
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if([[request URL].absoluteString isEqualToString:@"about:blank"])
{
return NO;
}
BOOL isFrame = ![[[request URL] absoluteString] isEqualToString:[[request mainDocumentURL] absoluteString]];
if(!isFrame)
{
self.lastLoadDate = [NSDate date];
self.previousMainFrameURL = self.mainFrameURL;
self.mainFrameURL = request.URL.absoluteString;
if(!self.anchorChangeInProgress)
{
[self updateStopReloadButtonState];
[self setToolbarHidden:NO];
}
}
if(!self.anchorChangeInProgress && [[[request URL] scheme] isCaseInsensitiveEqual:@"file"])
{
BOOL isMain = [request.URL isEqual:request.mainDocumentURL];
NSMutableURLRequest *mutRequest = (id)request;
NSString *url = [[[request URL] absoluteString] stringByReplacingOccurrencesOfString:@"file://" withString:@"dash-tarix://"];
NSURL *newURL = [NSURL URLWithString:url];
[mutRequest setURL:newURL];
if(isMain)
{
[mutRequest setMainDocumentURL:newURL];
}
[self performSelector:@selector(updateBackForwardButtonState) withObject:self afterDelay:0.1];
return YES;
}
if(navigationType == UIWebViewNavigationTypeLinkClicked || navigationType == UIWebViewNavigationTypeFormSubmitted || navigationType == UIWebViewNavigationTypeFormResubmitted)
{
[[DHRemoteServer sharedServer] sendWebViewURL:[request URL].absoluteString];
}
[self performSelector:@selector(updateBackForwardButtonState) withObject:self afterDelay:0.1];
return YES;
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
if([self stopButtonIsShown])
{
[self updateStopReloadButtonState];
}
[self updateBackForwardButtonState];
[self updateTitle];
[self setUpScripts];
[self setUpTOC];
[self.progressView setProgress:1.0 animated:YES];
if (self.isRestoreScroll) {
[self.webView.scrollView setContentOffset:self.webViewOffset animated:NO];
self.isRestoreScroll = NO;
}
}
- (void)setUpTOC
{
if([DHRemoteServer sharedServer].connectedRemote || [[self loadedURL] hasPrefix:@"dash-apple-api://"])
{
return;
}
self.lastTocBrowser = nil;
NSString *path = [[[[[self loadedURL] substringFromString:@"://"] substringToString:@"#"] substringToString:@"?"] stringByReplacingPercentEscapes];
NSString *tocPath = [path stringByAppendingString:@".dashtoc"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSData *jsonData = nil;
if([fileManager fileExistsAtPath:tocPath])
{
jsonData = [NSData dataWithContentsOfFile:tocPath];
}
else
{
jsonData = [DHUnarchiver tarixReadFile:tocPath toFile:nil];
}
NSDictionary *json = (jsonData) ? [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil] : nil;
if(json && [json isKindOfClass:[NSDictionary class]])
{
NSArray *methods = json[@"entries"];
if([DHCSS activeAppleLanguage] == DHActiveAppleLanguageSwift && json[@"entries_swift"])
{
methods = json[@"entries_swift"];
}
NSMutableArray *actualMethods = [NSMutableArray array];
for(NSDictionary *method in methods)
{
if([method[@"isSpacer"] boolValue])
{
continue;
}
[actualMethods addObject:method];
}
self.currentMethods = actualMethods;
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"tocMenu"] style:UIBarButtonItemStylePlain target:self action:@selector(tocButtonPressed:)];
if(json[@"title"] && [[json[@"title"] trimWhitespace] length])
{
self.title = [json[@"title"] trimWhitespace];
}
}
else
{
self.navigationItem.rightBarButtonItem = nil;
}
}
- (void)snippetUseButtonPressed:(id)sender
{
[[DHRemoteServer sharedServer] sendObject:@{@"selector": @"useSnippet"} forRequestName:@"performWebSelector" encrypted:YES toMacName:[DHRemoteServer sharedServer].connectedRemote.name];
}
- (void)tocButtonPressed:(id)sender
{
if(iPad && isRegularHorizontalClass)
{
if([self dismissMethodsPopoverIfVisible:YES])
{
return;
}
id tocBrowser = (self.lastTocBrowser && [self.lastTocBrowser isKindOfClass:[DHTocBrowser class]]) ? self.lastTocBrowser : [self.storyboard instantiateViewControllerWithIdentifier:@"DHTocBrowser"];
if(self.lastTocBrowser != tocBrowser)
{
self.lastTocBrowser = tocBrowser;
[self prepareTocBrowser:tocBrowser];
}
self.methodsPopover = [[UIPopoverController alloc] initWithContentViewController:tocBrowser];
[tocBrowser setPreferredContentSize:CGSizeMake(400, 600)];
[self.methodsPopover presentPopoverFromBarButtonItem:self.navigationItem.rightBarButtonItem permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
else
{
id tocBrowser = (self.lastTocBrowser && [self.lastTocBrowser isKindOfClass:[UINavigationController class]]) ? self.lastTocBrowser : [self.storyboard instantiateViewControllerWithIdentifier:@"DHTocBrowserNavigationController"];
if(self.lastTocBrowser != tocBrowser)
{
self.lastTocBrowser = tocBrowser;
[self prepareTocBrowser:(id)[tocBrowser topViewController]];
}
[self presentViewController:tocBrowser animated:YES completion:nil];
}
}
- (void)prepareTocBrowser:(DHTocBrowser *)tocBrowser
{
tocBrowser.title = self.title;
NSMutableArray *sections = [NSMutableArray array];
NSMutableArray *sectionTitles = [NSMutableArray array];
for(NSDictionary *method in self.currentMethods)
{
if([method[@"isHeader"] boolValue])
{
[sectionTitles addObject:method[@"name"]];
[sections addObject:[NSMutableArray array]];
}
else
{
[[sections lastObject] addObject:method];
}
}
[tocBrowser setSections:sections];
[tocBrowser setSectionTitles:sectionTitles];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if([self stopButtonIsShown])
{
[self updateStopReloadButtonState];
}
NSString *customMessage = nil;
[self updateTitle];
if([error code] == NSURLErrorCancelled || ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 204) || ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102 && ![[[error userInfo][NSURLErrorFailingURLStringErrorKey] substringFromString:@"://"] isCaseInsensitiveEqual:[self.mainFrameURL substringFromString:@"://"]]))
{
return;
}
else if([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102)
{
customMessage = @"Invalid URL.";
}
self.mainFrameURL = self.previousMainFrameURL;
[self.progressView setProgress:1.0 animated:YES];
[[[UIAlertView alloc] initWithTitle:@"Error Loading Page" message:(customMessage) ? customMessage : error.localizedDescription delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
- (JSContext *)jsContext
{
JSContext *ctx = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
return ctx;
}
- (void)setUpScripts
{
self.jsContext[@"window"][@"alert"] = [DHJavaScriptBridge sharedBridge].alertBlock;
self.jsContext[@"window"][@"dash"] = [DHJavaScriptBridge sharedBridge];
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] javaScriptInFile:@"scroll_to_current_anchor"]];
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] javaScriptInFile:@"hash_change_notifier"]];
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] javaScriptInFile:@"on_page_load"]];
NSString *platform = nil;
DHDocset *docset = nil;
if([DHRemoteServer sharedServer].connectedRemote)
{
platform = [DHRemoteProtocol lastResponseUserInfo][@"platform"];
}
else
{
docset = [[DHDocsetManager sharedManager] docsetForDocumentationPage:self.mainFrameURL];
platform = docset.platform;
}
if(!platform && [self.mainFrameURL contains:@"developer.apple.com"])
{
platform = @"osx";
}
if(platform)
{
if([platform isEqualToString:@"macosx"] || [platform isEqualToString:@"osx"] || [platform isEqualToString:@"iphoneos"] || [platform isEqualToString:@"ios"] || [platform isEqualToString:@"watchos"] || [platform isEqualToString:@"tvos"])
{
[self setUpCocoaScripts:docset];
}
else if([platform isEqualToString:@"apple"])
{
[self setUpAppleScripts:docset];
}
else if([platform isEqualToString:@"rails"])
{
[self setUpRailsScripts:docset];
}
else if([platform isEqualToString:@"ruby"])
{
[self setUpRubyScripts:docset];
}
else if([platform isEqualToString:@"net"])
{
[self setUpMSDNScripts:docset];
}
else if([platform isEqualToString:@"unity3d"])
{
[self setUpUnityScripts:docset];
}
}
if([self.mainFrameURL hasCaseInsensitivePrefix:@"http"] && [self.mainFrameURL contains:@"developer.apple.com"])
{
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] injectCSSScript]];
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] injectViewPortScript]];
}
}
- (void)setUpAppleScripts:(DHDocset *)docset
{
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] javaScriptInFile:@"apple"]];
}
- (void)setUpCocoaScripts:(DHDocset *)docset
{
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] javaScriptInFile:@"cocoa"]];
}
- (void)setUpRailsScripts:(DHDocset *)docset
{
[self.webView stringByEvaluatingJavaScriptFromString:@"function toggleSource(e){var t=$('#'+e).toggle();var n=t.is(':visible');$('#l_'+e).html(n?'hide':'show')}$(function(){$('.description pre').each(function(){hljs.highlightBlock(this)})})"];
}
- (void)setUpRubyScripts:(DHDocset *)docset
{
[self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] javaScriptInFile:@"ruby"]];
}
- (void)setUpMSDNScripts:(DHDocset *)docset
{
// [self.webView stringByEvaluatingJavaScriptFromString:[[DHJavaScript sharedJavaScript] javaScriptInFile:@"msdn"]];
}
- (void)setUpUnityScripts:(DHDocset *)docset
{
NSString *prefLanguage = [[NSUserDefaults standardUserDefaults] objectForKey:@"unitySelectedSnippetLanguage"];
if(prefLanguage)
{
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"var uls=document.getElementsByClassName('cSelectWidth');var found=false;for(var i=0;i scrollView.frame.size.height && ![self stopButtonIsShown] && [[NSDate date] timeIntervalSinceDate:self.lastLoadDate] > 0.1)
{
[self setToolbarHidden:YES];
}
else if(self.navigationController.toolbarHidden && [scrollView.panGestureRecognizer translationInView:self.view].y > 0.0f && ![self stopButtonIsShown])
{
[self setToolbarHidden:NO];
}
}
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
self.ignoreScroll = YES;
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale
{
self.ignoreScroll = NO;
}
- (void)updateBackForwardButtonState
{
self.backButton.enabled = self.webView.canGoBack;
self.forwardButton.enabled = self.webView.canGoForward;
}
- (void)updateStopReloadButtonState
{
if((self.webView.loading && ![self stopButtonIsShown]) || (!self.webView.loading && [self stopButtonIsShown]))
{
self.stopButtonIsShown = self.webView.loading;
// NSMutableArray *items = [self.toolbarItems mutableCopy];
// [items replaceObjectAtIndex:[items count]-1 withObject:(self.webView.loading) ? self.stopButton : self.reloadButton];
// [self setToolbarItems:items animated:YES];
}
}
- (void)loadURL:(NSString *)urlString
{
NSString *anchor = [[urlString substringFromStringReturningNil:@"#"] stringByReplacingOccurrencesOfString:@" " withString:@"%20"];
NSString *urlWithoutAnchor = [[urlString substringToString:@"#"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
urlString = (anchor) ? [urlWithoutAnchor stringByAppendingFormat:@"#%@", anchor] : urlWithoutAnchor;
if(![urlString hasCaseInsensitivePrefix:@"file://"] && [urlString hasPrefix:@"/"])
{
urlString = [@"dash-tarix://" stringByAppendingString:urlString];
urlWithoutAnchor = [@"dash-tarix://" stringByAppendingString:urlWithoutAnchor];
}
urlString = [urlString stringByReplacingOccurrencesOfString:@"file://" withString:@"dash-tarix://"];
urlWithoutAnchor = [urlWithoutAnchor stringByReplacingOccurrencesOfString:@"file://" withString:@"dash-tarix://"];
NSURL *url = [NSURL URLWithString:urlString];
if(!url && anchor.length)
{
urlString = [urlWithoutAnchor stringByAppendingFormat:@"#%@", [anchor stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
url = [NSURL URLWithString:urlString];
}
if(url)
{
if(!self.webView.isLoading && [[[[self.mainFrameURL substringFromString:@"://"] stringByDeletingPathFragment] stringByReplacingPercentEscapes] isCaseInsensitiveEqual:[[[urlString substringFromString:@"://"] stringByDeletingPathFragment] stringByReplacingPercentEscapes]] && ![self.mainFrameURL contains:@"dash-remote-snippet://"])
{
self.nextAnchorChangeNotCausedByUserNavigation = YES;
self.anchorChangeInProgress = YES;
self.ignoreScroll = YES;
NSString *hash = [urlString pathFragment];
if(hash && hash.length)
{
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"if(window.location.hash == \"#%@\") { window.location.hash = ''; } window.location.href = \"#%@\"", hash, hash]];
}
else
{
[self.webView stringByEvaluatingJavaScriptFromString:@"scroll(0,0)"];
}
self.ignoreScroll = NO;
[self removeDashClearedClass];
self.anchorChangeInProgress = NO;
if(self.navigationController.toolbarHidden && ![DHRemoteServer sharedServer].connectedRemote)
{
self.ignoreScroll = YES;
[self.navigationController setToolbarHidden:NO animated:YES];
self.ignoreScroll = NO;
}
return;
}
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
}
}
- (void)removeDashClearedClass
{
if([[self.webView stringByEvaluatingJavaScriptFromString:@"document.body.className"] contains:@"dash_cleared"])
{
[self.webView stringByEvaluatingJavaScriptFromString:@"document.body.className = document.body.className.replace(/\\bdash_cleared\\b/, \" \");"];
self.title = [[self.webView stringByEvaluatingJavaScriptFromString:@"document.title"] trimWhitespace];
}
}
- (NSString *)loadedURL
{
return self.webView.request.URL.absoluteString;
}
- (void)reload
{
[self.webView stringByEvaluatingJavaScriptFromString:@"location.reload(true)"];
}
- (void)updateTitle
{
NSString *newTitle = self.pageTitle;
if(![self.title isEqualToString:newTitle])
{
self.title = self.pageTitle;
[self.actualTOCBrowser setTitle:self.title];
}
}
- (DHTocBrowser *)actualTOCBrowser
{
return [self.lastTocBrowser isKindOfClass:[DHTocBrowser class]] ? self.lastTocBrowser : [self.lastTocBrowser topViewController];
}
- (NSString *)pageTitle
{
NSString *title = [[self.webView stringByEvaluatingJavaScriptFromString:@"document.title"] trimWhitespace];
if(!title.length && [[self loadedURL] contains:@"://"])
{
title = [[[[self mainFrameURL] stringByDeletingPathFragment] lastPathComponent] stringByDeletingPathExtension];
title = (title.length) ? title : @"No Title";
}
return title;
}
- (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController*)fromVC toViewController:(UIViewController*)toVC
{
Class class = [DHWebViewController class];
BOOL fromIs = ![fromVC isKindOfClass:class];
BOOL toIs = ![toVC isKindOfClass:class];
if((fromIs && !toIs) || (toIs && !fromIs))
{
return [[DHNavigationAnimator alloc] init];
}
else if(fromIs && toIs)
{
DHNavigationAnimator *animator = [[DHNavigationAnimator alloc] init];
animator.noAnimation = YES;
return animator;
}
return nil;
}
- (void)toggleSplitView:(id)sender
{
[self.webView.window.rootViewController.view endEditing:YES];
[UIView animateWithDuration:0.3 animations:^{
if(self.splitViewController.preferredDisplayMode == UISplitViewControllerDisplayModePrimaryHidden)
{
[sender setImage:[UIImage imageNamed:@"expand"]];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAllVisible];
}
else
{
[sender setImage:[UIImage imageNamed:@"collapse"]];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
}
}];
}
- (BOOL)isLocalURL
{
if(!self.mainFrameURL)
{
return NO;
}
if([[self mainFrameURL] rangeOfString:@"file://"].location != NSNotFound || [[self mainFrameURL] hasPrefix:@"dash-stack://"] || [[self mainFrameURL] hasPrefix:@"dash-apple-api://"] || [[self mainFrameURL] hasPrefix:@"dash-tarix://"])
{
return YES;
}
return NO;
}
- (void)prepareForURLSearch:(id)sender
{
if(iPad && isRegularHorizontalClass)
{
[self dismissMethodsPopoverIfVisible:YES];
}
if(isRegularHorizontalClass)
{
if(self.splitViewController.preferredDisplayMode == UISplitViewControllerDisplayModePrimaryHidden)
{
[self.toggleSplitViewButton setImage:[UIImage imageNamed:@"expand"]];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAllVisible];
}
}
}
- (BOOL)dismissMethodsPopoverIfVisible:(BOOL)animated
{
if(self.methodsPopover.popoverVisible)
{
[self.methodsPopover dismissPopoverAnimated:animated];
return YES;
}
return NO;
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
[coder encodeObject:self.webView.request.URL.absoluteString forKey:@"webViewURL"];
[coder encodeObject:homePath forKey:@"homePath"];
[coder encodeCGPoint:self.webView.scrollView.contentOffset forKey:@"webViewOffset"];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
[super decodeRestorableStateWithCoder:coder];
NSString *loadURL = [coder decodeObjectForKey:@"webViewURL"];
NSString *lastHomePath = [coder decodeObjectForKey:@"homePath"];
self.webViewOffset = [coder decodeCGPointForKey:@"webViewOffset"];
if (lastHomePath) {
loadURL = [[loadURL stringByReplacingOccurrencesOfString:lastHomePath withString:homePath] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}
self.isRestoring = YES;
[self viewDidLoad];
self.isRestoring = NO;
self.isDecoding = YES;
[self loadURL:loadURL];
if (isRegularHorizontalClass) {
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAllVisible];
}
self.isRestoreScroll = YES;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
}
static inline UIBarButtonItem *UIBarButtonWithFixedWidth(CGFloat width)
{
UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
button.width = width;
return button;
}
+ (instancetype)sharedWebViewController
{
if(singleton)
{
return singleton;
}
id webViewController = [[DHAppDelegate mainStoryboard] instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
return webViewController;
}
+ (id)alloc
{
if(singleton)
{
return singleton;
}
return [super alloc];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if(singleton)
{
return singleton;
}
self = [super initWithCoder:aDecoder];
singleton = self;
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.navigationController.delegate = nil;
self.webView.scrollView.delegate = nil;
self.webView.delegate = nil;
[self.webView loadHTMLString:@"" baseURL:nil];
[self.webView stopLoading];
[self.webView removeFromSuperview];
}
@end
================================================
FILE: Dash/DHWindow.h
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import
@interface DHWindow : UIWindow
@end
================================================
FILE: Dash/DHWindow.m
================================================
//
// Copyright (C) 2016 Kapeli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
#import "DHWindow.h"
@implementation DHWindow
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
[[NSNotificationCenter defaultCenter] postNotificationName:DHWindowChangedTraitCollection object:nil];
}
@end
================================================
FILE: Dash/Dash iOS.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
26480C2B1E2E9ABB00E3145E /* docset_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 26480C2A1E2E9ABB00E3145E /* docset_icon.png */; };
3A2F71C185D6471649E8F3A7 /* libPods-Dash-Dash App Store.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FB91D369AD27465BED375AD /* libPods-Dash-Dash App Store.a */; };
4303A3BC19C6584F00A7CD85 /* whitespace_tokenizer.c in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B119C6584F00A7CD85 /* whitespace_tokenizer.c */; };
4303A3BD19C6584F00A7CD85 /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B319C6584F00A7CD85 /* FMDatabase.m */; };
4303A3BE19C6584F00A7CD85 /* FMDatabaseAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B519C6584F00A7CD85 /* FMDatabaseAdditions.m */; };
4303A3BF19C6584F00A7CD85 /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B719C6584F00A7CD85 /* FMResultSet.m */; };
4303A3C019C6584F00A7CD85 /* FMDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B919C6584F00A7CD85 /* FMDatabasePool.m */; };
4303A3C119C6584F00A7CD85 /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3BB19C6584F00A7CD85 /* FMDatabaseQueue.m */; };
4303A3C419C658B000A7CD85 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 4303A3C319C658B000A7CD85 /* libsqlite3.dylib */; };
4303A3C719C6590000A7CD85 /* DHDocset.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3C619C6590000A7CD85 /* DHDocset.m */; };
4303A3CD19C65E6A00A7CD85 /* DHDocsetIndexer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3CC19C65E6A00A7CD85 /* DHDocsetIndexer.m */; };
4303A3D119C668BC00A7CD85 /* DHType.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3D019C668BC00A7CD85 /* DHType.m */; };
4303A3D419C668C200A7CD85 /* DHTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3D319C668C200A7CD85 /* DHTypes.m */; };
430C768B19E52B7F006F0DE0 /* DHTarixProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 430C768A19E52B7F006F0DE0 /* DHTarixProtocol.m */; };
430C768D19E55E6E006F0DE0 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 430C768C19E55E6E006F0DE0 /* MobileCoreServices.framework */; };
431108D519C7AE6D0050174C /* DHUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 431108D419C7AE6D0050174C /* DHUnarchiver.m */; };
431173DC19DA3513007E238E /* DHImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 431173DB19DA3513007E238E /* DHImageCache.m */; };
432321E21AA1BBF70045BA77 /* DHRemoteBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 432321E11AA1BBF70045BA77 /* DHRemoteBrowser.m */; };
43256F0719F7A6A8003919BA /* DHBrowserTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 43256F0619F7A6A8003919BA /* DHBrowserTableView.m */; };
43256F0A19F7B4B9003919BA /* UITableView+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43256F0919F7B4B9003919BA /* UITableView+DHUtils.m */; };
43280F541DDE4A7B001C2062 /* DHUserRepo.m in Sources */ = {isa = PBXBuildFile; fileRef = 43280F531DDE4A7B001C2062 /* DHUserRepo.m */; };
43328E3619EDA0D8000D44AE /* scroll_to_current_anchor.js in Resources */ = {isa = PBXBuildFile; fileRef = 43328E3419EDA0D8000D44AE /* scroll_to_current_anchor.js */; };
43352F6519C7A69900D4B1D8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 43352F6419C7A69900D4B1D8 /* libz.dylib */; };
4338AB601AAF70AF007B85DE /* DHWebProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4338AB5F1AAF70AF007B85DE /* DHWebProgressView.m */; };
433BDDAE1DC3BE7700B0B145 /* ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 433BDDAD1DC3BE7700B0B145 /* ionicons.ttf */; };
433BDDB21DC3C3AC00B0B145 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 433BDDB11DC3C3AC00B0B145 /* Settings.bundle */; };
43418C281A0B63B00014EFB6 /* DHWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 43418C271A0B63B00014EFB6 /* DHWebView.m */; };
4341A1F619BFE89A00AD164A /* DHDocsetTransferrer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4341A1F519BFE89A00AD164A /* DHDocsetTransferrer.m */; };
4342C8A519E93FF200BE66A2 /* DHNestedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4342C8A419E93FF200BE66A2 /* DHNestedViewController.m */; };
434A77DC1BAE590C00CCF84B /* DHSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 434A77DB1BAE590C00CCF84B /* DHSplitViewController.m */; };
434AB15719C2738900791E07 /* DHRepoTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 434AB15619C2738900791E07 /* DHRepoTableViewCell.m */; };
435145971DEB7035000874A8 /* DHCheatRepo.m in Sources */ = {isa = PBXBuildFile; fileRef = 435145961DEB7035000874A8 /* DHCheatRepo.m */; };
4351459A1DEB7099000874A8 /* DHCheatRepoList.m in Sources */ = {isa = PBXBuildFile; fileRef = 435145991DEB7099000874A8 /* DHCheatRepoList.m */; };
43573F781A997EC7000E6F2D /* DHRemoteServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 43573F771A997EC7000E6F2D /* DHRemoteServer.m */; };
4361651D19CCF8500026F08E /* NSTimer+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 4361651C19CCF8500026F08E /* NSTimer+DHUtils.m */; };
436DF78019FFA926008814F8 /* DHRepoTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 436DF77F19FFA926008814F8 /* DHRepoTableView.m */; };
436F3E4319D4F617007BBC25 /* DHRightDetailLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 436F3E4219D4F617007BBC25 /* DHRightDetailLabel.m */; };
436FDF5E19FE57CD00553931 /* SourceCodePro-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5819FE57CD00553931 /* SourceCodePro-Black.ttf */; };
436FDF5F19FE57CD00553931 /* SourceCodePro-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5919FE57CD00553931 /* SourceCodePro-Bold.ttf */; };
436FDF6019FE57CD00553931 /* SourceCodePro-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5A19FE57CD00553931 /* SourceCodePro-ExtraLight.ttf */; };
436FDF6119FE57CD00553931 /* SourceCodePro-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5B19FE57CD00553931 /* SourceCodePro-Light.ttf */; };
436FDF6219FE57CD00553931 /* SourceCodePro-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5C19FE57CD00553931 /* SourceCodePro-Regular.ttf */; };
436FDF6319FE57CD00553931 /* SourceCodePro-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5D19FE57CD00553931 /* SourceCodePro-Semibold.ttf */; };
437699A719D8FD3D008B37F4 /* DHTransferFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 437699A619D8FD3D008B37F4 /* DHTransferFeed.m */; };
437AD41719E26C9500BA0FE4 /* DHBrowserCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 437AD41619E26C9500BA0FE4 /* DHBrowserCell.xib */; };
437B6F8719EBC98A003023E8 /* DHCSS.m in Sources */ = {isa = PBXBuildFile; fileRef = 437B6F8619EBC98A003023E8 /* DHCSS.m */; };
437B6F8919EBCBCA003023E8 /* style.css in Resources */ = {isa = PBXBuildFile; fileRef = 437B6F8819EBCBCA003023E8 /* style.css */; };
437FE06619E2AC0D0022ADF4 /* DHTypeBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 437FE06519E2AC0D0022ADF4 /* DHTypeBrowser.m */; };
437FE06819E2B4010022ADF4 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437FE06719E2B4010022ADF4 /* WebKit.framework */; };
438525E119C129FC002BAFD1 /* DHFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525E019C129FC002BAFD1 /* DHFeed.m */; };
438525E519C13E42002BAFD1 /* NSArray+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525E419C13E42002BAFD1 /* NSArray+DHUtils.m */; };
438525E819C13ECE002BAFD1 /* NSString+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525E719C13ECE002BAFD1 /* NSString+DHUtils.m */; };
438525EC19C150AC002BAFD1 /* DHDBResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525EB19C150AC002BAFD1 /* DHDBResult.m */; };
438B06DF19FFA2DF009A539D /* Launch.xib in Resources */ = {isa = PBXBuildFile; fileRef = 438B06DE19FFA2DF009A539D /* Launch.xib */; };
438B45B91A10BB6F0047A156 /* DHJavaScriptBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 438B45B81A10BB6F0047A156 /* DHJavaScriptBridge.m */; };
438D58C219E575D600012881 /* DHLoadingCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 438D58C119E575D600012881 /* DHLoadingCell.xib */; };
438E864A1D1D4A3C00FB6CD8 /* apple.js in Resources */ = {isa = PBXBuildFile; fileRef = 438E86491D1D4A3C00FB6CD8 /* apple.js */; };
438F622419D5E81A00AF71BC /* NSFileManager+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 438F622319D5E81A00AF71BC /* NSFileManager+DHUtils.m */; };
439051F619E67D840083A55E /* DHBrowserTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051F519E67D840083A55E /* DHBrowserTableViewCell.m */; };
439051F919E684160083A55E /* DHEntryBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051F819E684160083A55E /* DHEntryBrowser.m */; };
439051FC19E68FE20083A55E /* NSString+GTM.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051FB19E68FE20083A55E /* NSString+GTM.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
4390520019E692D90083A55E /* DHDBResultSorter.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051FF19E692D90083A55E /* DHDBResultSorter.m */; };
43915EE919C5D2FD002806FF /* UIView+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43915EE819C5D2FD002806FF /* UIView+DHUtils.m */; };
4398D34619BF55B500457CAF /* DHPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 4398D34519BF55B500457CAF /* DHPreferences.m */; };
43A1C2642001700600512DB3 /* Apple_Docs_Framework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43A1C2632001700600512DB3 /* Apple_Docs_Framework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
43A1C269200175B900512DB3 /* DHAppleAPIProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A1C268200175B900512DB3 /* DHAppleAPIProtocol.m */; };
43A1C26A200175B900512DB3 /* DHAppleAPIProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A1C268200175B900512DB3 /* DHAppleAPIProtocol.m */; };
43A1C26D200175E500512DB3 /* Apple_Docs_Framework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A1C2632001700600512DB3 /* Apple_Docs_Framework.framework */; };
43A1C26E200175E500512DB3 /* Apple_Docs_Framework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43A1C2632001700600512DB3 /* Apple_Docs_Framework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
43A356CB19FFCE4D0073D143 /* UIViewController+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A356CA19FFCE4D0073D143 /* UIViewController+DHUtils.m */; };
43A6D9C919E0D69700780BC8 /* DHDocsetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A6D9C819E0D69700780BC8 /* DHDocsetManager.m */; };
43AAB7821AAE776800DB76F3 /* DHRemoteProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 43AAB7811AAE776800DB76F3 /* DHRemoteProtocol.m */; };
43AB65341ED19D2F00A6EC18 /* DHAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2EF519BB8D87007E594D /* DHAppDelegate.m */; };
43AB65351ED19D2F00A6EC18 /* DHType.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3D019C668BC00A7CD85 /* DHType.m */; };
43AB65361ED19D2F00A6EC18 /* UIViewController+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A356CA19FFCE4D0073D143 /* UIViewController+DHUtils.m */; };
43AB65371ED19D2F00A6EC18 /* NSObject+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43BB044519ED1BB8000FBC0D /* NSObject+DHUtils.m */; };
43AB65381ED19D2F00A6EC18 /* UIView+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43915EE819C5D2FD002806FF /* UIView+DHUtils.m */; };
43AB65391ED19D2F00A6EC18 /* NSString+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525E719C13ECE002BAFD1 /* NSString+DHUtils.m */; };
43AB653A1ED19D2F00A6EC18 /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B319C6584F00A7CD85 /* FMDatabase.m */; };
43AB653B1ED19D2F00A6EC18 /* DHPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 4398D34519BF55B500457CAF /* DHPreferences.m */; };
43AB653C1ED19D2F00A6EC18 /* DHTypeBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 437FE06519E2AC0D0022ADF4 /* DHTypeBrowser.m */; };
43AB653D1ED19D2F00A6EC18 /* DHCheatRepo.m in Sources */ = {isa = PBXBuildFile; fileRef = 435145961DEB7035000874A8 /* DHCheatRepo.m */; };
43AB653E1ED19D2F00A6EC18 /* NSURL+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43CF8AF019D5DA060010E698 /* NSURL+DHUtils.m */; };
43AB653F1ED19D2F00A6EC18 /* DHFeedResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD19E19C52E3400A0EBEC /* DHFeedResult.m */; };
43AB65401ED19D2F00A6EC18 /* DHWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2F0119BB8D87007E594D /* DHWebViewController.m */; };
43AB65411ED19D2F00A6EC18 /* NSData+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DCAEB71A9C3C8500A28A95 /* NSData+DHUtils.m */; };
43AB65421ED19D2F00A6EC18 /* FMDatabaseAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B519C6584F00A7CD85 /* FMDatabaseAdditions.m */; };
43AB65431ED19D2F00A6EC18 /* DHRepoTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 436DF77F19FFA926008814F8 /* DHRepoTableView.m */; };
43AB65441ED19D2F00A6EC18 /* DHBlockProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F25F9B19EBDEF30025CDC4 /* DHBlockProtocol.m */; };
43AB65451ED19D2F00A6EC18 /* DHRepo.m in Sources */ = {isa = PBXBuildFile; fileRef = 43B0956B19BFD57500F8611B /* DHRepo.m */; };
43AB65461ED19D2F00A6EC18 /* DHWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D7E4EF1BB4C62E00FC1DEF /* DHWindow.m */; };
43AB65471ED19D2F00A6EC18 /* DHAppUpdateChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 43BD6BF31DCE379B0005A1E7 /* DHAppUpdateChecker.m */; };
43AB65481ED19D2F00A6EC18 /* NSString+GTM.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051FB19E68FE20083A55E /* NSString+GTM.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
43AB65491ED19D2F00A6EC18 /* DHDBSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06619E7D68100C22AF5 /* DHDBSearcher.m */; };
43AB654A1ED19D2F00A6EC18 /* DHRemoteImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DFED821A9EE104008F7795 /* DHRemoteImage.m */; };
43AB654B1ED19D2F00A6EC18 /* DHAppleActiveLanguage.m in Sources */ = {isa = PBXBuildFile; fileRef = 43AF0E141D1861CE00F0D8A0 /* DHAppleActiveLanguage.m */; };
43AB654C1ED19D2F00A6EC18 /* DHBrowserTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 43256F0619F7A6A8003919BA /* DHBrowserTableView.m */; };
43AB654D1ED19D2F00A6EC18 /* DHRemote.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DFED7F1A9EBEA5008F7795 /* DHRemote.m */; };
43AB654E1ED19D2F00A6EC18 /* DHWebProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4338AB5F1AAF70AF007B85DE /* DHWebProgressView.m */; };
43AB654F1ED19D2F00A6EC18 /* DHDocset.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3C619C6590000A7CD85 /* DHDocset.m */; };
43AB65501ED19D2F00A6EC18 /* UITableView+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43256F0919F7B4B9003919BA /* UITableView+DHUtils.m */; };
43AB65511ED19D2F00A6EC18 /* NSArray+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525E419C13E42002BAFD1 /* NSArray+DHUtils.m */; };
43AB65521ED19D2F00A6EC18 /* DHDBNestedResultSorter.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06919E7DF3900C22AF5 /* DHDBNestedResultSorter.m */; };
43AB65531ED19D2F00A6EC18 /* DHCSS.m in Sources */ = {isa = PBXBuildFile; fileRef = 437B6F8619EBC98A003023E8 /* DHCSS.m */; };
43AB65541ED19D2F00A6EC18 /* DHDBResultSorter.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051FF19E692D90083A55E /* DHDBResultSorter.m */; };
43AB65551ED19D2F00A6EC18 /* DHRemoteBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 432321E11AA1BBF70045BA77 /* DHRemoteBrowser.m */; };
43AB65561ED19D2F00A6EC18 /* FMDatabaseQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3BB19C6584F00A7CD85 /* FMDatabaseQueue.m */; };
43AB65571ED19D2F00A6EC18 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2EF119BB8D87007E594D /* main.m */; };
43AB65581ED19D2F00A6EC18 /* DHTocBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D28EBA19F97D5500FDC5EE /* DHTocBrowser.m */; };
43AB65591ED19D2F00A6EC18 /* DHDocsetDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 43B0956E19BFDBDC00F8611B /* DHDocsetDownloader.m */; };
43AB655A1ED19D2F00A6EC18 /* UIButton+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F7527219CD1C2000D76F58 /* UIButton+DHUtils.m */; };
43AB655B1ED19D2F00A6EC18 /* DHSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 434A77DB1BAE590C00CCF84B /* DHSplitViewController.m */; };
43AB655C1ED19D2F00A6EC18 /* DHWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 43418C271A0B63B00014EFB6 /* DHWebView.m */; };
43AB655D1ED19D2F00A6EC18 /* DHBrowserTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051F519E67D840083A55E /* DHBrowserTableViewCell.m */; };
43AB655E1ED19D2F00A6EC18 /* DHDocsetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 43A6D9C819E0D69700780BC8 /* DHDocsetManager.m */; };
43AB655F1ED19D2F00A6EC18 /* DHTransferFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 437699A619D8FD3D008B37F4 /* DHTransferFeed.m */; };
43AB65601ED19D2F00A6EC18 /* NSFileManager+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 438F622319D5E81A00AF71BC /* NSFileManager+DHUtils.m */; };
43AB65611ED19D2F00A6EC18 /* whitespace_tokenizer.c in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B119C6584F00A7CD85 /* whitespace_tokenizer.c */; };
43AB65621ED19D2F00A6EC18 /* DHJavaScriptBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 438B45B81A10BB6F0047A156 /* DHJavaScriptBridge.m */; };
43AB65631ED19D2F00A6EC18 /* NSTimer+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 4361651C19CCF8500026F08E /* NSTimer+DHUtils.m */; };
43AB65641ED19D2F00A6EC18 /* DHTarixProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 430C768A19E52B7F006F0DE0 /* DHTarixProtocol.m */; };
43AB65651ED19D2F00A6EC18 /* DHTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3D319C668C200A7CD85 /* DHTypes.m */; };
43AB65661ED19D2F00A6EC18 /* FMDatabasePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B919C6584F00A7CD85 /* FMDatabasePool.m */; };
43AB65671ED19D2F00A6EC18 /* DHDocsetTransferrer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4341A1F519BFE89A00AD164A /* DHDocsetTransferrer.m */; };
43AB65681ED19D2F00A6EC18 /* DHSearchDisplayController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F760B119EA61B5002BD8A6 /* DHSearchDisplayController.m */; settings = {COMPILER_FLAGS = "-w"; }; };
43AB65691ED19D2F00A6EC18 /* DHNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DF51931A07B4C0006AB295 /* DHNavigationController.m */; };
43AB656A1ED19D2F00A6EC18 /* DHRepoTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 434AB15619C2738900791E07 /* DHRepoTableViewCell.m */; };
43AB656B1ED19D2F00A6EC18 /* DHFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525E019C129FC002BAFD1 /* DHFeed.m */; };
43AB656C1ED19D2F00A6EC18 /* DHRemoteServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 43573F771A997EC7000E6F2D /* DHRemoteServer.m */; };
43AB656D1ED19D2F00A6EC18 /* DHLatencyTestResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD19919C51E9C00A0EBEC /* DHLatencyTestResult.m */; };
43AB656E1ED19D2F00A6EC18 /* DHDBSearchController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06C19E7E88400C22AF5 /* DHDBSearchController.m */; };
43AB656F1ED19D2F00A6EC18 /* DHCheatRepoList.m in Sources */ = {isa = PBXBuildFile; fileRef = 435145991DEB7099000874A8 /* DHCheatRepoList.m */; };
43AB65701ED19D2F00A6EC18 /* DHQueuedDB.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06319E7CEC500C22AF5 /* DHQueuedDB.m */; };
43AB65711ED19D2F00A6EC18 /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3B719C6584F00A7CD85 /* FMResultSet.m */; };
43AB65721ED19D2F00A6EC18 /* DHUserRepoList.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FC34011DE784C40089F134 /* DHUserRepoList.m */; };
43AB65731ED19D2F00A6EC18 /* DHUserRepo.m in Sources */ = {isa = PBXBuildFile; fileRef = 43280F531DDE4A7B001C2062 /* DHUserRepo.m */; };
43AB65741ED19D2F00A6EC18 /* DHNavigationAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = 43B0956819BFD19900F8611B /* DHNavigationAnimator.m */; };
43AB65751ED19D2F00A6EC18 /* DHEntryBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 439051F819E684160083A55E /* DHEntryBrowser.m */; };
43AB65761ED19D2F00A6EC18 /* DHUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 431108D419C7AE6D0050174C /* DHUnarchiver.m */; };
43AB65771ED19D2F00A6EC18 /* DHDocsetIndexer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3CC19C65E6A00A7CD85 /* DHDocsetIndexer.m */; };
43AB65781ED19D2F00A6EC18 /* DHRightDetailLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 436F3E4219D4F617007BBC25 /* DHRightDetailLabel.m */; };
43AB65791ED19D2F00A6EC18 /* DHTarixIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F84A7519EEBD1A008FE85C /* DHTarixIndex.m */; };
43AB657A1ED19D2F00A6EC18 /* DHJavaScript.m in Sources */ = {isa = PBXBuildFile; fileRef = 43BB043819ED07DD000FBC0D /* DHJavaScript.m */; };
43AB657B1ED19D2F00A6EC18 /* DHDBResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 438525EB19C150AC002BAFD1 /* DHDBResult.m */; };
43AB657C1ED19D2F00A6EC18 /* DHNestedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4342C8A419E93FF200BE66A2 /* DHNestedViewController.m */; };
43AB657D1ED19D2F00A6EC18 /* DHFileDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD1A119C5309300A0EBEC /* DHFileDownload.m */; };
43AB657E1ED19D2F00A6EC18 /* DHLatencyTester.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD19719C51E9C00A0EBEC /* DHLatencyTester.m */; };
43AB657F1ED19D2F00A6EC18 /* DHImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 431173DB19DA3513007E238E /* DHImageCache.m */; };
43AB65801ED19D2F00A6EC18 /* DHDocsetBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2EFE19BB8D87007E594D /* DHDocsetBrowser.m */; };
43AB65811ED19D2F00A6EC18 /* DHRemoteProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 43AAB7811AAE776800DB76F3 /* DHRemoteProtocol.m */; };
43AB65831ED19D2F00A6EC18 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43BB043E19ED0B19000FBC0D /* JavaScriptCore.framework */; };
43AB65841ED19D2F00A6EC18 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 430C768C19E55E6E006F0DE0 /* MobileCoreServices.framework */; };
43AB65851ED19D2F00A6EC18 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 437FE06719E2B4010022ADF4 /* WebKit.framework */; };
43AB65861ED19D2F00A6EC18 /* libarchive.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 43EE932919CD079600FFF30C /* libarchive.a */; };
43AB65871ED19D2F00A6EC18 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 43352F6419C7A69900D4B1D8 /* libz.dylib */; };
43AB65881ED19D2F00A6EC18 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 4303A3C319C658B000A7CD85 /* libsqlite3.dylib */; };
43AB65891ED19D2F00A6EC18 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43FD2EE719BB8D87007E594D /* CoreGraphics.framework */; };
43AB658A1ED19D2F00A6EC18 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43FD2EE919BB8D87007E594D /* UIKit.framework */; };
43AB658B1ED19D2F00A6EC18 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43FD2EE519BB8D87007E594D /* Foundation.framework */; };
43AB658E1ED19D2F00A6EC18 /* SourceCodePro-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5A19FE57CD00553931 /* SourceCodePro-ExtraLight.ttf */; };
43AB658F1ED19D2F00A6EC18 /* SourceCodePro-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5C19FE57CD00553931 /* SourceCodePro-Regular.ttf */; };
43AB65901ED19D2F00A6EC18 /* on_page_load.js in Resources */ = {isa = PBXBuildFile; fileRef = 43EED91B1B607DB800F3DC35 /* on_page_load.js */; };
43AB65911ED19D2F00A6EC18 /* ClassRuby.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045019ED48BD000FBC0D /* ClassRuby.png */; };
43AB65921ED19D2F00A6EC18 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43FD2EFA19BB8D87007E594D /* Main.storyboard */; };
43AB65931ED19D2F00A6EC18 /* Launch.xib in Resources */ = {isa = PBXBuildFile; fileRef = 438B06DE19FFA2DF009A539D /* Launch.xib */; };
43AB65941ED19D2F00A6EC18 /* cocoa.js in Resources */ = {isa = PBXBuildFile; fileRef = 43BB044019ED0C86000FBC0D /* cocoa.js */; };
43AB65951ED19D2F00A6EC18 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43FD2F0319BB8D87007E594D /* Images.xcassets */; };
43AB65961ED19D2F00A6EC18 /* DHRepoCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 43F7527419CD31D100D76F58 /* DHRepoCell.xib */; };
43AB65971ED19D2F00A6EC18 /* SourceCodePro-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5819FE57CD00553931 /* SourceCodePro-Black.ttf */; };
43AB65981ED19D2F00A6EC18 /* DHLoadingCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 438D58C119E575D600012881 /* DHLoadingCell.xib */; };
43AB65991ED19D2F00A6EC18 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 433BDDB11DC3C3AC00B0B145 /* Settings.bundle */; };
43AB659A1ED19D2F00A6EC18 /* SourceCodePro-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5D19FE57CD00553931 /* SourceCodePro-Semibold.ttf */; };
43AB659B1ED19D2F00A6EC18 /* ruby.js in Resources */ = {isa = PBXBuildFile; fileRef = 43BB044B19ED3E78000FBC0D /* ruby.js */; };
43AB659C1ED19D2F00A6EC18 /* SourceCodePro-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5919FE57CD00553931 /* SourceCodePro-Bold.ttf */; };
43AB659D1ED19D2F00A6EC18 /* SourceCodePro-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 436FDF5B19FE57CD00553931 /* SourceCodePro-Light.ttf */; };
43AB659E1ED19D2F00A6EC18 /* Defaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = 43F70ABF19CBBD20000849C8 /* Defaults.plist */; };
43AB659F1ED19D2F00A6EC18 /* ModuleRuby.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045219ED48BD000FBC0D /* ModuleRuby.png */; };
43AB65A01ED19D2F00A6EC18 /* home.html in Resources */ = {isa = PBXBuildFile; fileRef = 43AC700019E4530400E0DB43 /* home.html */; };
43AB65A11ED19D2F00A6EC18 /* style.css in Resources */ = {isa = PBXBuildFile; fileRef = 437B6F8819EBCBCA003023E8 /* style.css */; };
43AB65A21ED19D2F00A6EC18 /* DHBrowserCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 437AD41619E26C9500BA0FE4 /* DHBrowserCell.xib */; };
43AB65A31ED19D2F00A6EC18 /* hash_change_notifier.js in Resources */ = {isa = PBXBuildFile; fileRef = 43BD91ED1AB31A9E001058C4 /* hash_change_notifier.js */; };
43AB65A41ED19D2F00A6EC18 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 43FD2EEE19BB8D87007E594D /* InfoPlist.strings */; };
43AB65A51ED19D2F00A6EC18 /* ModuleRubyRetina.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045319ED48BD000FBC0D /* ModuleRubyRetina.png */; };
43AB65A61ED19D2F00A6EC18 /* docset_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 26480C2A1E2E9ABB00E3145E /* docset_icon.png */; };
43AB65A71ED19D2F00A6EC18 /* ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 433BDDAD1DC3BE7700B0B145 /* ionicons.ttf */; };
43AB65A81ED19D2F00A6EC18 /* ClassRubyRetina.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045119ED48BD000FBC0D /* ClassRubyRetina.png */; };
43AB65A91ED19D2F00A6EC18 /* apple.js in Resources */ = {isa = PBXBuildFile; fileRef = 438E86491D1D4A3C00FB6CD8 /* apple.js */; };
43AB65AA1ED19D2F00A6EC18 /* scroll_to_current_anchor.js in Resources */ = {isa = PBXBuildFile; fileRef = 43328E3419EDA0D8000D44AE /* scroll_to_current_anchor.js */; };
43AB65AB1ED19D2F00A6EC18 /* msdn.js in Resources */ = {isa = PBXBuildFile; fileRef = 43B98F6219EE740F00C46BD9 /* msdn.js */; };
43AC700119E4530400E0DB43 /* home.html in Resources */ = {isa = PBXBuildFile; fileRef = 43AC700019E4530400E0DB43 /* home.html */; };
43AF0E151D1861CE00F0D8A0 /* DHAppleActiveLanguage.m in Sources */ = {isa = PBXBuildFile; fileRef = 43AF0E141D1861CE00F0D8A0 /* DHAppleActiveLanguage.m */; };
43B0956919BFD19900F8611B /* DHNavigationAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = 43B0956819BFD19900F8611B /* DHNavigationAnimator.m */; };
43B0956C19BFD57500F8611B /* DHRepo.m in Sources */ = {isa = PBXBuildFile; fileRef = 43B0956B19BFD57500F8611B /* DHRepo.m */; };
43B0956F19BFDBDC00F8611B /* DHDocsetDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 43B0956E19BFDBDC00F8611B /* DHDocsetDownloader.m */; };
43B98F6419EE740F00C46BD9 /* msdn.js in Resources */ = {isa = PBXBuildFile; fileRef = 43B98F6219EE740F00C46BD9 /* msdn.js */; };
43BB043919ED07DD000FBC0D /* DHJavaScript.m in Sources */ = {isa = PBXBuildFile; fileRef = 43BB043819ED07DD000FBC0D /* DHJavaScript.m */; };
43BB043F19ED0B19000FBC0D /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43BB043E19ED0B19000FBC0D /* JavaScriptCore.framework */; };
43BB044219ED0C86000FBC0D /* cocoa.js in Resources */ = {isa = PBXBuildFile; fileRef = 43BB044019ED0C86000FBC0D /* cocoa.js */; };
43BB044619ED1BB8000FBC0D /* NSObject+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43BB044519ED1BB8000FBC0D /* NSObject+DHUtils.m */; };
43BB044D19ED3E78000FBC0D /* ruby.js in Resources */ = {isa = PBXBuildFile; fileRef = 43BB044B19ED3E78000FBC0D /* ruby.js */; };
43BB045419ED48BD000FBC0D /* ClassRuby.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045019ED48BD000FBC0D /* ClassRuby.png */; };
43BB045519ED48BD000FBC0D /* ClassRubyRetina.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045119ED48BD000FBC0D /* ClassRubyRetina.png */; };
43BB045619ED48BD000FBC0D /* ModuleRuby.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045219ED48BD000FBC0D /* ModuleRuby.png */; };
43BB045719ED48BD000FBC0D /* ModuleRubyRetina.png in Resources */ = {isa = PBXBuildFile; fileRef = 43BB045319ED48BD000FBC0D /* ModuleRubyRetina.png */; };
43BCC3E4200442CA00DE24FE /* Apple_Docs_Framework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A1C2632001700600512DB3 /* Apple_Docs_Framework.framework */; };
43BD6BF41DCE379B0005A1E7 /* DHAppUpdateChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 43BD6BF31DCE379B0005A1E7 /* DHAppUpdateChecker.m */; };
43BD91EE1AB31A9E001058C4 /* hash_change_notifier.js in Resources */ = {isa = PBXBuildFile; fileRef = 43BD91ED1AB31A9E001058C4 /* hash_change_notifier.js */; };
43CF8AF119D5DA060010E698 /* NSURL+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43CF8AF019D5DA060010E698 /* NSURL+DHUtils.m */; };
43D28EBB19F97D5500FDC5EE /* DHTocBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D28EBA19F97D5500FDC5EE /* DHTocBrowser.m */; };
43D4F06419E7CEC500C22AF5 /* DHQueuedDB.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06319E7CEC500C22AF5 /* DHQueuedDB.m */; };
43D4F06719E7D68100C22AF5 /* DHDBSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06619E7D68100C22AF5 /* DHDBSearcher.m */; };
43D4F06A19E7DF3900C22AF5 /* DHDBNestedResultSorter.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06919E7DF3900C22AF5 /* DHDBNestedResultSorter.m */; };
43D4F06D19E7E88400C22AF5 /* DHDBSearchController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D4F06C19E7E88400C22AF5 /* DHDBSearchController.m */; };
43D7E4F01BB4C62E00FC1DEF /* DHWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 43D7E4EF1BB4C62E00FC1DEF /* DHWindow.m */; };
43DCAEB81A9C3C8500A28A95 /* NSData+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DCAEB71A9C3C8500A28A95 /* NSData+DHUtils.m */; };
43DF51941A07B4C0006AB295 /* DHNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DF51931A07B4C0006AB295 /* DHNavigationController.m */; };
43DFED801A9EBEA5008F7795 /* DHRemote.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DFED7F1A9EBEA5008F7795 /* DHRemote.m */; };
43DFED831A9EE104008F7795 /* DHRemoteImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 43DFED821A9EE104008F7795 /* DHRemoteImage.m */; };
43EDD19A19C51E9C00A0EBEC /* DHLatencyTester.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD19719C51E9C00A0EBEC /* DHLatencyTester.m */; };
43EDD19B19C51E9C00A0EBEC /* DHLatencyTestResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD19919C51E9C00A0EBEC /* DHLatencyTestResult.m */; };
43EDD19F19C52E3400A0EBEC /* DHFeedResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD19E19C52E3400A0EBEC /* DHFeedResult.m */; };
43EDD1A219C5309300A0EBEC /* DHFileDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 43EDD1A119C5309300A0EBEC /* DHFileDownload.m */; };
43EE932A19CD079600FFF30C /* libarchive.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 43EE932919CD079600FFF30C /* libarchive.a */; };
43EED91C1B607DB800F3DC35 /* on_page_load.js in Resources */ = {isa = PBXBuildFile; fileRef = 43EED91B1B607DB800F3DC35 /* on_page_load.js */; };
43F25F9C19EBDEF30025CDC4 /* DHBlockProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F25F9B19EBDEF30025CDC4 /* DHBlockProtocol.m */; };
43F31AAE200BB52F0082066C /* DHDocsetBrowserViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C39BFF200A836D00734571 /* DHDocsetBrowserViewModel.m */; };
43F70AC019CBBD20000849C8 /* Defaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = 43F70ABF19CBBD20000849C8 /* Defaults.plist */; };
43F7527319CD1C2000D76F58 /* UIButton+DHUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F7527219CD1C2000D76F58 /* UIButton+DHUtils.m */; };
43F7527519CD31D100D76F58 /* DHRepoCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 43F7527419CD31D100D76F58 /* DHRepoCell.xib */; };
43F760B219EA61B5002BD8A6 /* DHSearchDisplayController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F760B119EA61B5002BD8A6 /* DHSearchDisplayController.m */; settings = {COMPILER_FLAGS = "-w"; }; };
43F84A7619EEBD1A008FE85C /* DHTarixIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F84A7519EEBD1A008FE85C /* DHTarixIndex.m */; };
43FC34041DE784C40089F134 /* DHUserRepoList.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FC34011DE784C40089F134 /* DHUserRepoList.m */; };
43FD2EE619BB8D87007E594D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43FD2EE519BB8D87007E594D /* Foundation.framework */; };
43FD2EE819BB8D87007E594D /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43FD2EE719BB8D87007E594D /* CoreGraphics.framework */; };
43FD2EEA19BB8D87007E594D /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43FD2EE919BB8D87007E594D /* UIKit.framework */; };
43FD2EF019BB8D87007E594D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 43FD2EEE19BB8D87007E594D /* InfoPlist.strings */; };
43FD2EF219BB8D87007E594D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2EF119BB8D87007E594D /* main.m */; };
43FD2EF619BB8D87007E594D /* DHAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2EF519BB8D87007E594D /* DHAppDelegate.m */; };
43FD2EFC19BB8D87007E594D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43FD2EFA19BB8D87007E594D /* Main.storyboard */; };
43FD2EFF19BB8D87007E594D /* DHDocsetBrowser.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2EFE19BB8D87007E594D /* DHDocsetBrowser.m */; };
43FD2F0219BB8D87007E594D /* DHWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43FD2F0119BB8D87007E594D /* DHWebViewController.m */; };
43FD2F0419BB8D87007E594D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43FD2F0319BB8D87007E594D /* Images.xcassets */; };
45D28F8ABD1FC392B1930467 /* libPods-Dash.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2364AD2032B0714BCD2265 /* libPods-Dash.a */; };
F5C39C00200A836D00734571 /* DHDocsetBrowserViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C39BFF200A836D00734571 /* DHDocsetBrowserViewModel.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
43A1C24E200161D100512DB3 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
43A1C2642001700600512DB3 /* Apple_Docs_Framework.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
43A1C26F200175E500512DB3 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
43A1C26E200175E500512DB3 /* Apple_Docs_Framework.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
26480C2A1E2E9ABB00E3145E /* docset_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = docset_icon.png; sourceTree = ""; };
4303A3B019C6584F00A7CD85 /* whitespace_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = whitespace_tokenizer.h; sourceTree = ""; };
4303A3B119C6584F00A7CD85 /* whitespace_tokenizer.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = whitespace_tokenizer.c; sourceTree = ""; };
4303A3B219C6584F00A7CD85 /* FMDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabase.h; sourceTree = ""; };
4303A3B319C6584F00A7CD85 /* FMDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabase.m; sourceTree = ""; };
4303A3B419C6584F00A7CD85 /* FMDatabaseAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabaseAdditions.h; sourceTree = ""; };
4303A3B519C6584F00A7CD85 /* FMDatabaseAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseAdditions.m; sourceTree = ""; };
4303A3B619C6584F00A7CD85 /* FMResultSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMResultSet.h; sourceTree = ""; };
4303A3B719C6584F00A7CD85 /* FMResultSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMResultSet.m; sourceTree = ""; };
4303A3B819C6584F00A7CD85 /* FMDatabasePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabasePool.h; sourceTree = ""; };
4303A3B919C6584F00A7CD85 /* FMDatabasePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabasePool.m; sourceTree = ""; };
4303A3BA19C6584F00A7CD85 /* FMDatabaseQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FMDatabaseQueue.h; sourceTree = ""; };
4303A3BB19C6584F00A7CD85 /* FMDatabaseQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FMDatabaseQueue.m; sourceTree = ""; };
4303A3C319C658B000A7CD85 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; };
4303A3C519C6590000A7CD85 /* DHDocset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDocset.h; sourceTree = ""; };
4303A3C619C6590000A7CD85 /* DHDocset.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDocset.m; sourceTree = ""; };
4303A3CB19C65E6A00A7CD85 /* DHDocsetIndexer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDocsetIndexer.h; sourceTree = ""; };
4303A3CC19C65E6A00A7CD85 /* DHDocsetIndexer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDocsetIndexer.m; sourceTree = ""; };
4303A3CF19C668BC00A7CD85 /* DHType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHType.h; sourceTree = ""; };
4303A3D019C668BC00A7CD85 /* DHType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHType.m; sourceTree = ""; };
4303A3D219C668C200A7CD85 /* DHTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHTypes.h; sourceTree = ""; };
4303A3D319C668C200A7CD85 /* DHTypes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHTypes.m; sourceTree = ""; };
430C768919E52B7F006F0DE0 /* DHTarixProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHTarixProtocol.h; sourceTree = ""; };
430C768A19E52B7F006F0DE0 /* DHTarixProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHTarixProtocol.m; sourceTree = ""; };
430C768C19E55E6E006F0DE0 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
431108D319C7AE6D0050174C /* DHUnarchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHUnarchiver.h; sourceTree = ""; };
431108D419C7AE6D0050174C /* DHUnarchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHUnarchiver.m; sourceTree = ""; };
431173DA19DA3513007E238E /* DHImageCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHImageCache.h; sourceTree = ""; };
431173DB19DA3513007E238E /* DHImageCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHImageCache.m; sourceTree = ""; };
432321E01AA1BBF70045BA77 /* DHRemoteBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHRemoteBrowser.h; sourceTree = ""; };
432321E11AA1BBF70045BA77 /* DHRemoteBrowser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHRemoteBrowser.m; sourceTree = ""; };
43256F0519F7A6A8003919BA /* DHBrowserTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHBrowserTableView.h; sourceTree = ""; };
43256F0619F7A6A8003919BA /* DHBrowserTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHBrowserTableView.m; sourceTree = ""; };
43256F0819F7B4B9003919BA /* UITableView+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITableView+DHUtils.h"; sourceTree = ""; };
43256F0919F7B4B9003919BA /* UITableView+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITableView+DHUtils.m"; sourceTree = ""; };
43280F521DDE4A7B001C2062 /* DHUserRepo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHUserRepo.h; sourceTree = ""; };
43280F531DDE4A7B001C2062 /* DHUserRepo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHUserRepo.m; sourceTree = ""; };
43328E3419EDA0D8000D44AE /* scroll_to_current_anchor.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = scroll_to_current_anchor.js; sourceTree = ""; };
43352F5F19C7A67200D4B1D8 /* archive_entry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = archive_entry.h; sourceTree = ""; };
43352F6019C7A67200D4B1D8 /* archive.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = archive.h; sourceTree = ""; };
43352F6419C7A69900D4B1D8 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
4338AB5E1AAF70AF007B85DE /* DHWebProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHWebProgressView.h; sourceTree = ""; };
4338AB5F1AAF70AF007B85DE /* DHWebProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHWebProgressView.m; sourceTree = ""; };
433BDDAD1DC3BE7700B0B145 /* ionicons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ionicons.ttf; sourceTree = ""; };
433BDDB11DC3C3AC00B0B145 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; };
43418C261A0B63B00014EFB6 /* DHWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHWebView.h; sourceTree = ""; };
43418C271A0B63B00014EFB6 /* DHWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHWebView.m; sourceTree = ""; };
4341A1F419BFE89A00AD164A /* DHDocsetTransferrer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDocsetTransferrer.h; sourceTree = ""; };
4341A1F519BFE89A00AD164A /* DHDocsetTransferrer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDocsetTransferrer.m; sourceTree = ""; };
4342C8A319E93FF200BE66A2 /* DHNestedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHNestedViewController.h; sourceTree = ""; };
4342C8A419E93FF200BE66A2 /* DHNestedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHNestedViewController.m; sourceTree = ""; };
434A77DA1BAE590C00CCF84B /* DHSplitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHSplitViewController.h; sourceTree = ""; };
434A77DB1BAE590C00CCF84B /* DHSplitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHSplitViewController.m; sourceTree = ""; };
434AB15519C2738900791E07 /* DHRepoTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHRepoTableViewCell.h; sourceTree = ""; };
434AB15619C2738900791E07 /* DHRepoTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHRepoTableViewCell.m; sourceTree = ""; };
435145951DEB7035000874A8 /* DHCheatRepo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHCheatRepo.h; sourceTree = ""; };
435145961DEB7035000874A8 /* DHCheatRepo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHCheatRepo.m; sourceTree = ""; };
435145981DEB7099000874A8 /* DHCheatRepoList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHCheatRepoList.h; sourceTree = ""; };
435145991DEB7099000874A8 /* DHCheatRepoList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHCheatRepoList.m; sourceTree = ""; };
43573F761A997EC7000E6F2D /* DHRemoteServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHRemoteServer.h; sourceTree = ""; };
43573F771A997EC7000E6F2D /* DHRemoteServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHRemoteServer.m; sourceTree = ""; };
4361651B19CCF8500026F08E /* NSTimer+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSTimer+DHUtils.h"; sourceTree = ""; };
4361651C19CCF8500026F08E /* NSTimer+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSTimer+DHUtils.m"; sourceTree = ""; };
436DF77E19FFA926008814F8 /* DHRepoTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHRepoTableView.h; sourceTree = ""; };
436DF77F19FFA926008814F8 /* DHRepoTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHRepoTableView.m; sourceTree = ""; };
436F3E4119D4F617007BBC25 /* DHRightDetailLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHRightDetailLabel.h; sourceTree = ""; };
436F3E4219D4F617007BBC25 /* DHRightDetailLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHRightDetailLabel.m; sourceTree = ""; };
436FDF5819FE57CD00553931 /* SourceCodePro-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-Black.ttf"; sourceTree = ""; };
436FDF5919FE57CD00553931 /* SourceCodePro-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-Bold.ttf"; sourceTree = ""; };
436FDF5A19FE57CD00553931 /* SourceCodePro-ExtraLight.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-ExtraLight.ttf"; sourceTree = ""; };
436FDF5B19FE57CD00553931 /* SourceCodePro-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-Light.ttf"; sourceTree = ""; };
436FDF5C19FE57CD00553931 /* SourceCodePro-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-Regular.ttf"; sourceTree = ""; };
436FDF5D19FE57CD00553931 /* SourceCodePro-Semibold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SourceCodePro-Semibold.ttf"; sourceTree = ""; };
437699A519D8FD3D008B37F4 /* DHTransferFeed.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHTransferFeed.h; sourceTree = ""; };
437699A619D8FD3D008B37F4 /* DHTransferFeed.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHTransferFeed.m; sourceTree = ""; };
437AD41619E26C9500BA0FE4 /* DHBrowserCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DHBrowserCell.xib; sourceTree = ""; };
437B6F8519EBC98A003023E8 /* DHCSS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHCSS.h; sourceTree = ""; };
437B6F8619EBC98A003023E8 /* DHCSS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHCSS.m; sourceTree = ""; };
437B6F8819EBCBCA003023E8 /* style.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = style.css; sourceTree = ""; };
437FE06419E2AC0D0022ADF4 /* DHTypeBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHTypeBrowser.h; sourceTree = ""; };
437FE06519E2AC0D0022ADF4 /* DHTypeBrowser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHTypeBrowser.m; sourceTree = ""; };
437FE06719E2B4010022ADF4 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
438525DF19C129FC002BAFD1 /* DHFeed.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHFeed.h; sourceTree = ""; };
438525E019C129FC002BAFD1 /* DHFeed.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHFeed.m; sourceTree = ""; };
438525E319C13E42002BAFD1 /* NSArray+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+DHUtils.h"; sourceTree = ""; };
438525E419C13E42002BAFD1 /* NSArray+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+DHUtils.m"; sourceTree = ""; };
438525E619C13ECE002BAFD1 /* NSString+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+DHUtils.h"; sourceTree = ""; };
438525E719C13ECE002BAFD1 /* NSString+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+DHUtils.m"; sourceTree = ""; };
438525EA19C150AC002BAFD1 /* DHDBResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDBResult.h; sourceTree = ""; };
438525EB19C150AC002BAFD1 /* DHDBResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDBResult.m; sourceTree = ""; };
438B06DE19FFA2DF009A539D /* Launch.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = Launch.xib; sourceTree = ""; };
438B45B71A10BB6F0047A156 /* DHJavaScriptBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHJavaScriptBridge.h; sourceTree = ""; };
438B45B81A10BB6F0047A156 /* DHJavaScriptBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHJavaScriptBridge.m; sourceTree = ""; };
438D58C119E575D600012881 /* DHLoadingCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DHLoadingCell.xib; sourceTree = ""; };
438E86491D1D4A3C00FB6CD8 /* apple.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = apple.js; sourceTree = ""; };
438F622219D5E81A00AF71BC /* NSFileManager+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFileManager+DHUtils.h"; sourceTree = ""; };
438F622319D5E81A00AF71BC /* NSFileManager+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFileManager+DHUtils.m"; sourceTree = ""; };
439051F419E67D840083A55E /* DHBrowserTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHBrowserTableViewCell.h; sourceTree = ""; };
439051F519E67D840083A55E /* DHBrowserTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHBrowserTableViewCell.m; sourceTree = ""; };
439051F719E684160083A55E /* DHEntryBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHEntryBrowser.h; sourceTree = ""; };
439051F819E684160083A55E /* DHEntryBrowser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHEntryBrowser.m; sourceTree = ""; };
439051FA19E68FE20083A55E /* NSString+GTM.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+GTM.h"; sourceTree = ""; };
439051FB19E68FE20083A55E /* NSString+GTM.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+GTM.m"; sourceTree = ""; };
439051FE19E692D90083A55E /* DHDBResultSorter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDBResultSorter.h; sourceTree = ""; };
439051FF19E692D90083A55E /* DHDBResultSorter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDBResultSorter.m; sourceTree = ""; };
43915EE719C5D2FD002806FF /* UIView+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+DHUtils.h"; sourceTree = ""; };
43915EE819C5D2FD002806FF /* UIView+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+DHUtils.m"; sourceTree = ""; };
4398D34419BF55B500457CAF /* DHPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHPreferences.h; sourceTree = ""; };
4398D34519BF55B500457CAF /* DHPreferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHPreferences.m; sourceTree = ""; };
43A1C2632001700600512DB3 /* Apple_Docs_Framework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Apple_Docs_Framework.framework; path = ../Frameworks/Apple_Docs_Framework.framework; sourceTree = ""; };
43A1C267200175B900512DB3 /* DHAppleAPIProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DHAppleAPIProtocol.h; sourceTree = ""; };
43A1C268200175B900512DB3 /* DHAppleAPIProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DHAppleAPIProtocol.m; sourceTree = ""; };
43A356C919FFCE4D0073D143 /* UIViewController+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+DHUtils.h"; sourceTree = ""; };
43A356CA19FFCE4D0073D143 /* UIViewController+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+DHUtils.m"; sourceTree = ""; };
43A6D9C719E0D69700780BC8 /* DHDocsetManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDocsetManager.h; sourceTree = ""; };
43A6D9C819E0D69700780BC8 /* DHDocsetManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDocsetManager.m; sourceTree = ""; };
43AAB7801AAE776800DB76F3 /* DHRemoteProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHRemoteProtocol.h; sourceTree = ""; };
43AAB7811AAE776800DB76F3 /* DHRemoteProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHRemoteProtocol.m; sourceTree = ""; };
43AB65B21ED19D2F00A6EC18 /* Dash.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Dash.app; sourceTree = BUILT_PRODUCTS_DIR; };
43AC700019E4530400E0DB43 /* home.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = home.html; sourceTree = ""; };
43AF0E131D1861C600F0D8A0 /* DHAppleActiveLanguage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHAppleActiveLanguage.h; sourceTree = ""; };
43AF0E141D1861CE00F0D8A0 /* DHAppleActiveLanguage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHAppleActiveLanguage.m; sourceTree = ""; };
43B0956719BFD19900F8611B /* DHNavigationAnimator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHNavigationAnimator.h; sourceTree = ""; };
43B0956819BFD19900F8611B /* DHNavigationAnimator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHNavigationAnimator.m; sourceTree = ""; };
43B0956A19BFD57500F8611B /* DHRepo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHRepo.h; sourceTree = ""; };
43B0956B19BFD57500F8611B /* DHRepo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHRepo.m; sourceTree = ""; };
43B0956D19BFDBDC00F8611B /* DHDocsetDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDocsetDownloader.h; sourceTree = ""; };
43B0956E19BFDBDC00F8611B /* DHDocsetDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDocsetDownloader.m; sourceTree = ""; };
43B98F6219EE740F00C46BD9 /* msdn.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = msdn.js; sourceTree = ""; };
43BB043719ED07DD000FBC0D /* DHJavaScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHJavaScript.h; sourceTree = ""; };
43BB043819ED07DD000FBC0D /* DHJavaScript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHJavaScript.m; sourceTree = ""; };
43BB043E19ED0B19000FBC0D /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
43BB044019ED0C86000FBC0D /* cocoa.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = cocoa.js; sourceTree = ""; };
43BB044419ED1BB8000FBC0D /* NSObject+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+DHUtils.h"; sourceTree = ""; };
43BB044519ED1BB8000FBC0D /* NSObject+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+DHUtils.m"; sourceTree = ""; };
43BB044B19ED3E78000FBC0D /* ruby.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = ruby.js; sourceTree = ""; };
43BB045019ED48BD000FBC0D /* ClassRuby.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ClassRuby.png; sourceTree = ""; };
43BB045119ED48BD000FBC0D /* ClassRubyRetina.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ClassRubyRetina.png; sourceTree = ""; };
43BB045219ED48BD000FBC0D /* ModuleRuby.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ModuleRuby.png; sourceTree = ""; };
43BB045319ED48BD000FBC0D /* ModuleRubyRetina.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ModuleRubyRetina.png; sourceTree = ""; };
43BD6BF21DCE379B0005A1E7 /* DHAppUpdateChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHAppUpdateChecker.h; sourceTree = ""; };
43BD6BF31DCE379B0005A1E7 /* DHAppUpdateChecker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHAppUpdateChecker.m; sourceTree = ""; };
43BD91ED1AB31A9E001058C4 /* hash_change_notifier.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = hash_change_notifier.js; sourceTree = ""; };
43CF8AEF19D5DA060010E698 /* NSURL+DHUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+DHUtils.h"; sourceTree = ""; };
43CF8AF019D5DA060010E698 /* NSURL+DHUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+DHUtils.m"; sourceTree = ""; };
43D28EB919F97D5500FDC5EE /* DHTocBrowser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHTocBrowser.h; sourceTree = ""; };
43D28EBA19F97D5500FDC5EE /* DHTocBrowser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHTocBrowser.m; sourceTree = ""; };
43D4F06219E7CEC500C22AF5 /* DHQueuedDB.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHQueuedDB.h; sourceTree = ""; };
43D4F06319E7CEC500C22AF5 /* DHQueuedDB.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHQueuedDB.m; sourceTree = ""; };
43D4F06519E7D68100C22AF5 /* DHDBSearcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDBSearcher.h; sourceTree = ""; };
43D4F06619E7D68100C22AF5 /* DHDBSearcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDBSearcher.m; sourceTree = ""; };
43D4F06819E7DF3900C22AF5 /* DHDBNestedResultSorter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDBNestedResultSorter.h; sourceTree = ""; };
43D4F06919E7DF3900C22AF5 /* DHDBNestedResultSorter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DHDBNestedResultSorter.m; sourceTree = ""; };
43D4F06B19E7E88400C22AF5 /* DHDBSearchController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DHDBSearchController.h; sourceTree = "