2013-08-24 10:06:45 +00:00
|
|
|
/* global window */
|
|
|
|
(function(angular, _, moment){
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
// TODO Disqus
|
|
|
|
// TODO Show compatible emacs versions for any package
|
|
|
|
// TODO Google Analytics http://stackoverflow.com/questions/10713708/tracking-google-analytics-page-views-with-angular-js
|
|
|
|
// TODO D3 visualisation for deps
|
2013-08-27 08:10:57 +00:00
|
|
|
// TODO Fix json encoding of versions
|
2013-11-24 14:33:56 +00:00
|
|
|
// TODO Link to specific github branch
|
2013-10-06 07:51:15 +00:00
|
|
|
// TODO Show recent github events on package pages where applicable
|
|
|
|
// TODO Voting / starring
|
2013-08-24 10:06:45 +00:00
|
|
|
|
2013-09-28 07:15:04 +00:00
|
|
|
var app = angular.module('melpa', ["ngRoute"]);
|
2013-08-24 10:06:45 +00:00
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// SERVICES
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
app.factory('packageService', function($http, $q) {
|
|
|
|
var packages = $q.all({archive: $http.get("/archive.json"),
|
|
|
|
recipes: $http.get("/recipes.json"),
|
|
|
|
downloads: $http.get("/download_counts.json") // TODO: handle missing
|
|
|
|
})
|
|
|
|
.then(function (info) {
|
|
|
|
|
2013-09-27 17:40:05 +00:00
|
|
|
var calculateSourceURL = function(name, recipe) {
|
2013-08-24 10:06:45 +00:00
|
|
|
if (recipe.fetcher == "github") {
|
|
|
|
return (/\//.test(recipe.repo) ? "https://github.com/" : "https://gist.github.com/") + recipe.repo;
|
|
|
|
} else if (recipe.fetcher == "wiki" && !recipe.files) {
|
|
|
|
return "http://www.emacswiki.org/emacs/" + name + ".el";
|
|
|
|
} else if (recipe.url) {
|
|
|
|
var urlMatch = function(re, prefix) {
|
|
|
|
var m = recipe.url.match(re);
|
|
|
|
return m !== null ? prefix + m[0] : null;
|
|
|
|
};
|
|
|
|
return urlMatch(/(bitbucket\.org\/[^\/]+\/[^\/\?]+)/, "https://") ||
|
|
|
|
urlMatch(/(gitorious\.org\/[^\/]+\/[^.]+)/, "https://") ||
|
|
|
|
urlMatch(/\Alp:(.*)/, "https://launchpad.net/") ||
|
|
|
|
urlMatch(/\A(https?:\/\/code\.google\.com\/p\/[^\/]+\/)/) ||
|
|
|
|
urlMatch(/\A(https?:\/\/[^.]+\.googlecode\.com\/)/);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
2013-09-04 08:54:07 +00:00
|
|
|
var listed = _.intersection(_.keys(info.archive.data), _.keys(info.recipes.data));
|
2013-08-24 10:06:45 +00:00
|
|
|
return _(listed).reduce(function(pkgs, name) {
|
|
|
|
var built = info.archive.data[name];
|
|
|
|
var recipe = info.recipes.data[name];
|
|
|
|
var descr = built[2].replace(/\s*\[((?:source: )?\w+)\]$/, "");
|
|
|
|
var version = built[0].join(".");
|
|
|
|
// Fix up hokey deps, which look like {"clojure-mode":{"2":[0,0]}} for 2.0.0
|
2013-09-04 08:54:07 +00:00
|
|
|
var deps = _.map(built[1] || {}, function (val, name) {
|
2013-08-24 10:06:45 +00:00
|
|
|
var v1 = _.keys(val)[0];
|
|
|
|
return {name: name, version: [v1].concat(val[v1] || []).join('.')};
|
|
|
|
});
|
2013-10-17 07:31:15 +00:00
|
|
|
var oldNames = recipe['old-names'] || [];
|
2013-08-24 10:06:45 +00:00
|
|
|
pkgs[name] = {
|
|
|
|
name: name,
|
|
|
|
version: version,
|
|
|
|
dependencies: deps,
|
|
|
|
description: descr,
|
|
|
|
source: recipe.fetcher,
|
2013-10-17 07:31:15 +00:00
|
|
|
downloads: _.reduce(oldNames.concat(name), function(sum, n) { return sum + (info.downloads.data[n] || 0); }, 0),
|
2013-08-24 10:06:45 +00:00
|
|
|
fetcher: recipe.fetcher,
|
|
|
|
recipeURL: "https://github.com/milkypostman/melpa/blob/master/recipes/" + name,
|
|
|
|
packageURL: "packages/" + name + "-" + version + "." + (built[3] == "single" ? "el" : "tar"),
|
2013-10-17 07:31:15 +00:00
|
|
|
sourceURL: calculateSourceURL(name, recipe),
|
|
|
|
oldNames: oldNames
|
2013-08-24 10:06:45 +00:00
|
|
|
};
|
|
|
|
return pkgs;
|
|
|
|
}, {});
|
|
|
|
});
|
|
|
|
var buildStatus = $http.get("/build-status.json").then(function(r) { return r.data; });
|
|
|
|
|
|
|
|
return {
|
|
|
|
getPackages: function() {
|
|
|
|
return packages;
|
|
|
|
},
|
|
|
|
|
|
|
|
dependenciesOn: function(name) {
|
|
|
|
return packages.then(function(pkgs) {
|
2013-09-04 08:54:07 +00:00
|
|
|
return _.values(pkgs).filter(function(p) {
|
|
|
|
return _.findWhere(p.dependencies, {name: name});
|
2013-08-24 10:06:45 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
buildStatus: function() {
|
|
|
|
return buildStatus;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Controllers
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2013-10-14 16:25:12 +00:00
|
|
|
app.controller('PackageListCtrl', function ($scope, $timeout, packageService) {
|
2013-08-24 10:06:45 +00:00
|
|
|
$scope.orderBy = "name";
|
2013-10-14 16:25:12 +00:00
|
|
|
$scope.$on('$routeChangeSuccess', function (ev, current) {
|
|
|
|
$scope.enteredSearchTerms = $scope.searchTerms = current.params.q;
|
|
|
|
});
|
|
|
|
var copyValue;
|
|
|
|
$scope.$watch('enteredSearchTerms', function(value) {
|
|
|
|
$timeout.cancel(copyValue);
|
|
|
|
copyValue = $timeout(function() {
|
|
|
|
$scope.searchTerms = value;
|
|
|
|
}, 250);
|
|
|
|
});
|
|
|
|
|
2013-08-24 10:06:45 +00:00
|
|
|
packageService.getPackages().then(function(pkgs){
|
2013-09-04 08:54:07 +00:00
|
|
|
$scope.packages = _.values(pkgs);
|
|
|
|
$scope.totalDownloads = _.reduce(_.pluck($scope.packages, "downloads"), function (a, b) { return b === undefined ? a : a + b; }, 0);
|
2013-08-24 10:06:45 +00:00
|
|
|
});
|
|
|
|
$scope.packageMatcher = function(term) {
|
|
|
|
var t = term && term.toLowerCase();
|
2013-09-29 16:40:11 +00:00
|
|
|
var searchText = _.memoize(function(pkg) {
|
|
|
|
return _([pkg.name, pkg.description, pkg.source, pkg.version, pkg.sourceURL]).compact().invoke('toLowerCase').valueOf().join(' ');
|
|
|
|
}, function(pkg) { return pkg.name; });
|
2013-08-24 10:06:45 +00:00
|
|
|
return function(pkg) {
|
|
|
|
if (!term || !term.match(/\S/)) return true;
|
2013-09-27 17:40:05 +00:00
|
|
|
return searchText(pkg).indexOf(t) != -1;
|
2013-08-24 10:06:45 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2013-10-17 18:25:13 +00:00
|
|
|
var detailsCtrl = app.controller('PackageDetailsCtrl', function ($scope, $q, $routeParams, $http, packageService) {
|
2013-08-24 10:06:45 +00:00
|
|
|
var packageName = $routeParams.packageName;
|
|
|
|
packageService.getPackages().then(function(pkgs) {
|
|
|
|
$scope.allPackages = pkgs;
|
|
|
|
$scope.pkg = pkgs[packageName];
|
|
|
|
$scope.reverseDependencies = packageService.dependenciesOn(packageName);
|
2013-10-17 18:25:13 +00:00
|
|
|
$scope.reverseDependenciesOldNames = $q.all(_.map($scope.pkg.oldNames, packageService.dependenciesOn)).then(_.flatten);
|
2013-09-24 18:01:44 +00:00
|
|
|
var downloadCounts = _.pluck(pkgs, 'downloads');
|
|
|
|
$scope.downloadsPercentile = _.filter(downloadCounts, function(d) { return d < $scope.pkg.downloads; }).length * 100.0 / downloadCounts.length;
|
2013-08-24 10:06:45 +00:00
|
|
|
});
|
|
|
|
$scope.readme = $http.get("/packages/" + packageName + "-readme.txt").then(function(r) { return r.data; });
|
|
|
|
$scope.havePackage = function(pkgName) {
|
|
|
|
return $scope.allPackages[pkgName];
|
|
|
|
};
|
|
|
|
});
|
|
|
|
detailsCtrl.resolve = {
|
|
|
|
// Fail to resolve unless this particular package exists
|
|
|
|
pkgs: function (packageService, $route, $q) {
|
|
|
|
var deferred = $q.defer();
|
|
|
|
packageService.getPackages().then(function(pkgs) {
|
|
|
|
if (pkgs[$route.current.params.packageName])
|
|
|
|
deferred.resolve(pkgs);
|
|
|
|
else
|
|
|
|
deferred.reject("No such package");
|
|
|
|
});
|
|
|
|
return deferred.promise;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
app.controller('BuildStatusCtrl', function($scope, packageService) {
|
2013-08-25 10:08:20 +00:00
|
|
|
packageService.buildStatus().then(function(status) {
|
|
|
|
$scope.completionTime = new Date(status.completed * 1000);
|
|
|
|
});
|
2013-08-24 10:06:45 +00:00
|
|
|
});
|
|
|
|
|
2013-09-23 17:15:13 +00:00
|
|
|
app.controller('AppCtrl', function($scope, $rootScope, $route, $window) {
|
2013-08-27 08:10:57 +00:00
|
|
|
$scope.hideSplash = false;
|
2013-08-24 10:06:45 +00:00
|
|
|
$rootScope.$on("$routeChangeSuccess", function() {
|
2013-08-27 08:10:57 +00:00
|
|
|
$scope.hideSplash = ($route.current.controller != 'PackageListCtrl');
|
2013-08-24 10:06:45 +00:00
|
|
|
});
|
2013-09-23 17:15:13 +00:00
|
|
|
$rootScope.$on("$locationChangeSuccess", function(e, newURL, oldURL) {
|
2013-09-28 07:26:03 +00:00
|
|
|
//jshint unused: false
|
2013-09-23 17:15:13 +00:00
|
|
|
if (newURL && (newURL.$$route||newURL).redirectTo) return;
|
|
|
|
if ($window._gaq && newURL != (oldURL + "#/")) {
|
|
|
|
var l = $window.location;
|
|
|
|
var path = l.pathname + l.hash + l.search;
|
|
|
|
$window._gaq.push (["_trackPageview", path ]);
|
|
|
|
}
|
|
|
|
});
|
2013-08-24 10:06:45 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Directives
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
app.directive("viewOrError", function($rootScope) {
|
|
|
|
return {
|
|
|
|
template: '<div>' +
|
2013-08-24 16:17:19 +00:00
|
|
|
'<div class="alert alert-danger" ng-if="routeError"><strong>Error: </strong>{{routeError}}</div>' +
|
2013-09-28 07:15:04 +00:00
|
|
|
'<div ng-if="!routeError"><div ng-view><ng-transclude></ng-transclude></div></div>'+'</div>',
|
2013-08-24 10:06:45 +00:00
|
|
|
transclude: true,
|
|
|
|
link: function(scope) {
|
|
|
|
$rootScope.$on("$routeChangeError", function (event, current, previous, rejection) {
|
2013-09-28 07:26:03 +00:00
|
|
|
//jshint unused: false
|
2013-08-24 10:06:45 +00:00
|
|
|
scope.routeError = rejection;
|
|
|
|
});
|
|
|
|
$rootScope.$on("$routeChangeSuccess", function () {
|
|
|
|
scope.routeError = null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
app.directive("sortIndicator", function() {
|
|
|
|
return {
|
|
|
|
scope: { sortIndicator: "@" },
|
|
|
|
replace: true,
|
|
|
|
template: '<span ng-class="classname"></span>',
|
|
|
|
link: function(scope) {
|
|
|
|
scope.$watch('sortIndicator', function() {
|
|
|
|
scope.classname = 'table-sort-indicator glyphicon ' +
|
|
|
|
({'asc': 'glyphicon-chevron-down',
|
|
|
|
'desc': 'glyphicon-chevron-up' }[scope.sortIndicator] ||
|
|
|
|
'glyphicon-minus');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
app.directive("tableSort", function() {
|
|
|
|
return {
|
|
|
|
scope: {
|
|
|
|
tableSort: "=",
|
|
|
|
sortField: "@"
|
|
|
|
},
|
|
|
|
transclude: true,
|
2013-08-25 18:37:46 +00:00
|
|
|
template: '<div class="table-sort"></span><span ng-transclude></span><span sort-indicator="{{order}}"></div>',
|
2013-08-24 10:06:45 +00:00
|
|
|
link: function(scope, element) {
|
|
|
|
var sortingAsc = function() {
|
|
|
|
return _([scope.sortField, "+" + scope.sortField]).contains(scope.tableSort);
|
|
|
|
};
|
|
|
|
var sortingDesc = function() {
|
|
|
|
return scope.tableSort == "-" + scope.sortField;
|
|
|
|
};
|
|
|
|
element.bind('click', function() {
|
|
|
|
scope.$apply(function() {
|
|
|
|
scope.tableSort = sortingAsc() ? "-" + scope.sortField : scope.sortField;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
scope.$watch('tableSort', function() {
|
|
|
|
scope.order = (function() {
|
|
|
|
if (sortingAsc()) return 'asc';
|
|
|
|
if (sortingDesc()) return 'desc';
|
|
|
|
return '';
|
|
|
|
})();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Filters
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
app.filter('relativeTime', function() {
|
|
|
|
return function(val) {
|
2013-08-25 10:08:20 +00:00
|
|
|
return val && moment(val).fromNow();
|
2013-08-24 10:06:45 +00:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Routes
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
app.config(['$locationProvider', function($locationProvider){
|
|
|
|
$locationProvider.html5Mode(false);
|
|
|
|
}]);
|
|
|
|
app.config(['$routeProvider', function($routeProvider){
|
|
|
|
$routeProvider
|
|
|
|
.when('/', {templateUrl: 'partials/list.html', controller: 'PackageListCtrl', splash: true})
|
|
|
|
.when('/getting-started', {templateUrl: 'partials/getting-started.html'})
|
|
|
|
.when('/:packageName', {templateUrl: 'partials/package.html', controller: 'PackageDetailsCtrl', resolve: detailsCtrl.resolve});
|
|
|
|
}]);
|
|
|
|
|
|
|
|
})(window.angular, window._, window.moment);
|