mirror of
https://github.com/correl/melpa.git
synced 2024-11-15 03:00:14 +00:00
Merge branch 'mithril'
This commit is contained in:
commit
7b7bb8e177
5 changed files with 428 additions and 359 deletions
|
@ -14,9 +14,7 @@ pre code { /* Match highlight.js styles to bootstrap */
|
|||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.table-sort * {
|
||||
}
|
||||
.table-sort-indicator {
|
||||
#package-list th .glyphicon {
|
||||
padding-left: 1em;
|
||||
}
|
||||
#package-list th, #package-list td.version, #package-list td.recipe {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-US" ng-app="melpa" ng-controller="AppCtrl">
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>MELPA</title>
|
||||
|
||||
<meta name="description" lang="en" content="The largest and most up-to-date repository of Emacs packages."/>
|
||||
<link rel="shortcut icon" href="favicon.ico" />
|
||||
<link href="/updates.rss" rel="alternate" title="MELPA updates" type="application/rss+xml" />
|
||||
<!--[if lt IE 9]><script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.min.js" type="text/javascript"></script><![endif]-->
|
||||
<link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/melpa.css" type="text/css" />
|
||||
</head>
|
||||
|
@ -13,49 +14,36 @@
|
|||
<header class="navbar navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand">MELPA</a>
|
||||
<a href="#/" class="navbar-brand">MELPA</a>
|
||||
</div>
|
||||
<nav class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/">Packages</a></li>
|
||||
<li><a href="#getting-started">Getting started</a></li>
|
||||
<li><a href="#/">Packages</a></li>
|
||||
<li><a href="#/getting-started">Getting started</a></li>
|
||||
<li><a href="https://github.com/milkypostman/melpa">Github</a></li>
|
||||
<li><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=UCKDWNVGK8MFA&lc=US&item_name=MELPA¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted">Donate</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container">
|
||||
<div ng-show="!hideSplash">
|
||||
<!-- This content is handled separately from the main view for the benefit of bots and initial page load -->
|
||||
<section class="page-header">
|
||||
<h1>MELPA <small>(Milkypostman’s Emacs Lisp Package Archive)</small></h1>
|
||||
</section>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<section class="jumbotron">
|
||||
<ul>
|
||||
<li><strong>Up-to-date packages built on our servers from upstream source</strong></li>
|
||||
<li><strong>Installable in any recent Emacs using "package.el"</strong> - no need to install svn/cvs/hg/bzr/git/darcs etc.</li>
|
||||
<li><strong>Curated</strong> - no obsolete, renamed, forked or randomly hacked packages</li>
|
||||
<li><strong>Comprehensive</strong> - more packages than any other archive</li>
|
||||
<li><strong>Automatic updates</strong> - new commits result in new packages</li>
|
||||
<li><strong>Extensible</strong> - contribute recipes via github, and we'll build the packages</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="alert alert-success">
|
||||
<strong>Last build ended:</strong> <span ng-controller="BuildStatusCtrl" ng-bind="completionTime | relativeTime"></span>
|
||||
</div>
|
||||
<a class="twitter-timeline" data-dnt="true" data-related="milkypostman,sanityinc" href="https://twitter.com/melpa_emacs" data-widget-id="311867756586864640">Tweets by @melpa_emacs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div view-or-error></div>
|
||||
<main class="container" id="content">
|
||||
<noscript>
|
||||
<h1>MELPA (Milkypostman’s Emacs Lisp Package Archive)</h1>
|
||||
<ul>
|
||||
<li>Up-to-date packages built on our servers from upstream source</li>
|
||||
<li>Installable in any recent Emacs using 'package.el' - no need to install svn/cvs/hg/bzr/git/darcs etc.</li>
|
||||
<li>Curated - no obsolete, renamed, forked or randomly hacked packages</li>
|
||||
<li>Comprehensive - more packages than any other archive</li>
|
||||
<li>Automatic updates - new commits result in new packages</li>
|
||||
<li>Extensible - contribute recipes via github, and we'll build the packages</li>
|
||||
</ul>
|
||||
</noscript>
|
||||
</main>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js"></script>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular-route.min.js"></script>
|
||||
<!--[if lt IE 9]>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/es5-shim/2.3.0/es5-shim.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/json2/20130526/json2.min.js"></script>
|
||||
<![endif]-->
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/mithril/0.1.14/mithril.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.0.0/moment.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/1.3.1/lodash.min.js"></script>
|
||||
<script src="js/melpa.js"></script>
|
||||
|
|
642
html/js/melpa.js
642
html/js/melpa.js
|
@ -1,5 +1,5 @@
|
|||
/* global window */
|
||||
(function(angular, _, moment){
|
||||
(function(m, document, _, moment){
|
||||
"use strict";
|
||||
|
||||
// TODO Disqus
|
||||
|
@ -11,261 +11,427 @@
|
|||
// TODO Show recent github events on package pages where applicable
|
||||
// TODO Voting / starring
|
||||
|
||||
var app = angular.module('melpa', ["ngRoute"]);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// SERVICES
|
||||
// Helpers
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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) {
|
||||
|
||||
var calculateSourceURL = function(name, recipe) {
|
||||
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;
|
||||
};
|
||||
|
||||
var listed = _.intersection(_.keys(info.archive.data), _.keys(info.recipes.data));
|
||||
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
|
||||
var deps = _.map(built[1] || {}, function (val, name) {
|
||||
var v1 = _.keys(val)[0];
|
||||
return {name: name, version: [v1].concat(val[v1] || []).join('.')};
|
||||
});
|
||||
var oldNames = recipe['old-names'] || [];
|
||||
pkgs[name] = {
|
||||
name: name,
|
||||
version: version,
|
||||
dependencies: deps,
|
||||
description: descr,
|
||||
source: recipe.fetcher,
|
||||
downloads: _.reduce(oldNames.concat(name), function(sum, n) { return sum + (info.downloads.data[n] || 0); }, 0),
|
||||
fetcher: recipe.fetcher,
|
||||
recipeURL: "https://github.com/milkypostman/melpa/blob/master/recipes/" + name,
|
||||
packageURL: "packages/" + name + "-" + version + "." + (built[3] == "single" ? "el" : "tar"),
|
||||
sourceURL: calculateSourceURL(name, recipe),
|
||||
oldNames: oldNames
|
||||
};
|
||||
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) {
|
||||
return _.values(pkgs).filter(function(p) {
|
||||
return _.findWhere(p.dependencies, {name: name});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
buildStatus: function() {
|
||||
return buildStatus;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Controllers
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
app.controller('PackageListCtrl', function ($scope, $timeout, packageService) {
|
||||
$scope.orderBy = "name";
|
||||
$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);
|
||||
});
|
||||
|
||||
packageService.getPackages().then(function(pkgs){
|
||||
$scope.packages = _.values(pkgs);
|
||||
$scope.totalDownloads = _.reduce(_.pluck($scope.packages, "downloads"), function (a, b) { return b === undefined ? a : a + b; }, 0);
|
||||
});
|
||||
$scope.packageMatcher = function(term) {
|
||||
var t = term && term.toLowerCase();
|
||||
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; });
|
||||
return function(pkg) {
|
||||
if (!term || !term.match(/\S/)) return true;
|
||||
return searchText(pkg).indexOf(t) != -1;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
var detailsCtrl = app.controller('PackageDetailsCtrl', function ($scope, $q, $routeParams, $http, packageService) {
|
||||
var packageName = $routeParams.packageName;
|
||||
packageService.getPackages().then(function(pkgs) {
|
||||
$scope.allPackages = pkgs;
|
||||
$scope.pkg = pkgs[packageName];
|
||||
$scope.reverseDependencies = packageService.dependenciesOn(packageName);
|
||||
$scope.reverseDependenciesOldNames = $q.all(_.map($scope.pkg.oldNames, packageService.dependenciesOn)).then(_.flatten);
|
||||
var downloadCounts = _.pluck(pkgs, 'downloads');
|
||||
$scope.downloadsPercentile = _.filter(downloadCounts, function(d) { return d < $scope.pkg.downloads; }).length * 100.0 / downloadCounts.length;
|
||||
});
|
||||
$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;
|
||||
function intersperse(seq, sep) {
|
||||
var res = seq.slice(0,1);
|
||||
for(var i=1; i < seq.length; ++i) {
|
||||
res.push(sep);
|
||||
res.push(seq[i]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Models
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var melpa = {};
|
||||
|
||||
melpa.Package = function(data) {
|
||||
["name", "description", "version", "dependencies", "source",
|
||||
"downloads", "fetcher", "recipeURL", "packageURL", "sourceURL", "oldNames"].map(function(p) {
|
||||
this[p] = m.prop(data[p]);
|
||||
}.bind(this));
|
||||
this._searchText = _([data.name, data.description, data.source,
|
||||
data.version, data.sourceURL])
|
||||
.compact().valueOf().join(' ').toLowerCase();
|
||||
this.readmeURL = m.prop("/packages/" + data.name + "-readme.txt");
|
||||
this.matchesTerm = function(term) {
|
||||
return this._searchText.indexOf(term) != -1;
|
||||
};
|
||||
};
|
||||
|
||||
app.controller('BuildStatusCtrl', function($scope, packageService) {
|
||||
packageService.buildStatus().then(function(status) {
|
||||
$scope.completionTime = new Date(status.completed * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
app.controller('AppCtrl', function($scope, $rootScope, $route, $window) {
|
||||
$scope.hideSplash = false;
|
||||
$rootScope.$on("$routeChangeSuccess", function() {
|
||||
$scope.hideSplash = ($route.current.controller != 'PackageListCtrl');
|
||||
});
|
||||
$rootScope.$on("$locationChangeSuccess", function(e, newURL, oldURL) {
|
||||
//jshint unused: false
|
||||
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 ]);
|
||||
melpa.PackageList = function(packages) {
|
||||
this.packages = packages;
|
||||
this.totalDownloads = m.prop(_.reduce(_.map(packages, function(p) { return p.downloads() || 0; }),
|
||||
function (a, b) { return b === undefined ? a : a + b; }, 0));
|
||||
this.totalPackages = m.prop(packages.length);
|
||||
var savedSearches = {};
|
||||
function preFilteredPackages(term) {
|
||||
var prefixes = _.sortBy(_.filter(_.keys(savedSearches),
|
||||
function(k) { return term.indexOf(k) === 0; }),
|
||||
'length').reverse();
|
||||
return prefixes.length > 0 ? savedSearches[prefixes[0]] : null;
|
||||
}
|
||||
this.filteredPackages = function(terms, sortBy, sortAscending) {
|
||||
var t = terms.trim().toLowerCase();
|
||||
var packages = savedSearches[t];
|
||||
if (!packages) {
|
||||
var preFiltered = preFilteredPackages(t);
|
||||
packages = _.filter(preFilteredPackages(t) || this.packages,
|
||||
function(p) { return p.matchesTerm(t); });
|
||||
if (preFiltered) packages.sortKey = preFiltered.sortKey;
|
||||
}
|
||||
});
|
||||
});
|
||||
var sortKey = sortBy + "-" + sortAscending;
|
||||
if (packages.sortKey === sortKey) return packages;
|
||||
var matched = _.sortBy(packages, function(p) { return p[sortBy](); });
|
||||
packages = savedSearches[t] = sortAscending ? matched : matched.reverse();
|
||||
packages.sortKey = sortKey;
|
||||
return packages;
|
||||
};
|
||||
var packagesByName = {};
|
||||
_.each(packages, function(p) { packagesByName[p.name()] = p; });
|
||||
this.packageWithName = function(name) {
|
||||
return packagesByName[name];
|
||||
};
|
||||
|
||||
var downloadCounts = _.invoke(packages, 'downloads');
|
||||
this.downloadsPercentileForPackage = function(p) {
|
||||
return _.filter(downloadCounts, function(d) { return d < p.downloads(); }).length * 100.0 / downloadCounts.length;
|
||||
};
|
||||
|
||||
this.dependenciesOnPackageName = function(packageName) {
|
||||
return (_.filter(packages, function(p) {
|
||||
return _.findWhere(p.dependencies(), {name: packageName});
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Directives
|
||||
// Gather remote info about packages
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
app.directive("viewOrError", function($rootScope) {
|
||||
return {
|
||||
template: '<div>' +
|
||||
'<div class="alert alert-danger" ng-if="routeError"><strong>Error: </strong>{{routeError}}</div>' +
|
||||
'<div ng-if="!routeError"><div ng-view><ng-transclude></ng-transclude></div></div>'+'</div>',
|
||||
transclude: true,
|
||||
link: function(scope) {
|
||||
$rootScope.$on("$routeChangeError", function (event, current, previous, rejection) {
|
||||
//jshint unused: false
|
||||
scope.routeError = rejection;
|
||||
});
|
||||
$rootScope.$on("$routeChangeSuccess", function () {
|
||||
scope.routeError = null;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
melpa.packageList = m.sync([
|
||||
m.request({method: 'GET', url: "/recipes.json"}),
|
||||
m.request({method: 'GET', url: "/archive.json"}),
|
||||
m.request({method: 'GET', url: "/download_counts.json"})
|
||||
]).then(function (info) {
|
||||
var recipes = info[0], archive = info[1], downloads = info[2];
|
||||
|
||||
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,
|
||||
template: '<div class="table-sort"></span><span ng-transclude></span><span sort-indicator="{{order}}"></div>',
|
||||
link: function(scope, element) {
|
||||
var sortingAsc = function() {
|
||||
return _([scope.sortField, "+" + scope.sortField]).contains(scope.tableSort);
|
||||
var calculateSourceURL = function(name, recipe) {
|
||||
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;
|
||||
};
|
||||
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 '';
|
||||
})();
|
||||
});
|
||||
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;
|
||||
};
|
||||
|
||||
var listed = _.intersection(_.keys(archive), _.keys(recipes));
|
||||
return new melpa.PackageList(_(listed).reduce(function(pkgs, name) {
|
||||
var built = archive[name];
|
||||
var recipe = recipes[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
|
||||
var deps = _.map(built[1] || {}, function (val, name) {
|
||||
var v1 = _.keys(val)[0];
|
||||
return {name: name, version: [v1].concat(val[v1] || []).join('.')};
|
||||
});
|
||||
var oldNames = recipe['old-names'] || [];
|
||||
|
||||
pkgs.push(new melpa.Package({
|
||||
name: name,
|
||||
version: version,
|
||||
dependencies: deps,
|
||||
description: descr,
|
||||
source: recipe.fetcher,
|
||||
downloads: _.reduce(oldNames.concat(name), function(sum, n) { return sum + (downloads[n] || 0); }, 0),
|
||||
fetcher: recipe.fetcher,
|
||||
recipeURL: "https://github.com/milkypostman/melpa/blob/master/recipes/" + name,
|
||||
packageURL: "packages/" + name + "-" + version + "." + (built[3] == "single" ? "el" : "tar"),
|
||||
sourceURL: calculateSourceURL(name, recipe),
|
||||
oldNames: oldNames
|
||||
}));
|
||||
return pkgs;
|
||||
}, []));
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// View helpers
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function glyphicon(name) {
|
||||
return m("span.glyphicon.glyphicon-" + name);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Package list
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
melpa.packagelist = {};
|
||||
melpa.packagelist.controller = function() {
|
||||
this.filterTerms = m.prop(m.route.param('q') || '');
|
||||
this.sortBy = m.prop("name");
|
||||
this.sortAscending = m.prop(true);
|
||||
this.packageList = m.prop();
|
||||
melpa.packageList.then(this.packageList);
|
||||
this.filteredPackages = function() {
|
||||
return this.packageList().filteredPackages(this.filterTerms(), this.sortBy(), this.sortAscending());
|
||||
};
|
||||
this.toggleSort = function(field) {
|
||||
if (this.sortBy() == field) {
|
||||
this.sortAscending(!this.sortAscending());
|
||||
} else {
|
||||
this.sortAscending(true);
|
||||
this.sortBy(field);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Filters
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
app.filter('relativeTime', function() {
|
||||
return function(val) {
|
||||
return val && moment(val).fromNow();
|
||||
melpa.packagelist.view = function(ctrl) {
|
||||
var sortToggler = function(field) {
|
||||
return function() { return ctrl.toggleSort(field); };
|
||||
};
|
||||
var sortIndicator = function(field) {
|
||||
return glyphicon((field != ctrl.sortBy()) ? "minus" : (ctrl.sortAscending() ? "chevron-down" : "chevron-up"));
|
||||
};
|
||||
return m("section#packages", [
|
||||
m("h2", [
|
||||
"Current List of ",
|
||||
ctrl.packageList().totalPackages(),
|
||||
" Packages ",
|
||||
m("small", [
|
||||
ctrl.packageList().totalDownloads(),
|
||||
" downloads to date"
|
||||
])
|
||||
]),
|
||||
m("p", [
|
||||
m("input.form-control", {type: "search", placeholder: "Enter filter terms", autofocus: true,
|
||||
value: ctrl.filterTerms(), onkeyup: m.withAttr("value", ctrl.filterTerms)}),
|
||||
" ",
|
||||
m("span.help-block", ["Showing ", ctrl.filteredPackages().length, " matching package(s)"])
|
||||
]),
|
||||
m("table#package-list.table.table-bordered.table-responsive.table-hover", [
|
||||
m("thead", [
|
||||
m("tr", [
|
||||
m("th", {onclick: sortToggler("name")}, ["Package", sortIndicator("name")]),
|
||||
m("th", {onclick: sortToggler("description")}, ["Description", sortIndicator("description")]),
|
||||
m("th", {onclick: sortToggler("version")}, ["Version", sortIndicator("version")]),
|
||||
m("th", "Recipe"),
|
||||
m("th", {onclick: sortToggler("fetcher")}, ["Source", sortIndicator("fetcher")]),
|
||||
m("th", {onclick: sortToggler("downloads")}, ["DLs", sortIndicator("downloads")]),
|
||||
])
|
||||
]),
|
||||
m("tbody",
|
||||
ctrl.filteredPackages().map(function(p) {
|
||||
return m("tr", [
|
||||
m("td", [
|
||||
m("a", {href: "/" + p.name(), config: m.route}, [
|
||||
p.name()
|
||||
])
|
||||
]),
|
||||
m("td", [
|
||||
m("a", {href: "/" + p.name(), config: m.route}, [
|
||||
p.description()
|
||||
])
|
||||
]),
|
||||
m("td.version", [
|
||||
m("a", {href: p.packageURL()}, [
|
||||
p.version(),
|
||||
" ",
|
||||
glyphicon('download')
|
||||
])
|
||||
]),
|
||||
m("td.recipe", [
|
||||
m("a", {href: p.recipeURL()}, [
|
||||
glyphicon('cutlery')
|
||||
])
|
||||
]),
|
||||
m("td.source", [
|
||||
p.sourceURL() ? m("a", {href: p.sourceURL()}, [p.source()]) : p.source()
|
||||
]),
|
||||
m("td", [p.downloads()])
|
||||
]);
|
||||
}))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Package details
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
melpa.packagedetails = {};
|
||||
melpa.packagedetails.controller = function() {
|
||||
var packageName = m.route.param("package");
|
||||
this.package = m.prop();
|
||||
this.readme = m.prop('No description available.');
|
||||
this.neededBy = m.prop([]);
|
||||
this.downloadsPercentile = m.prop(0);
|
||||
|
||||
melpa.packageList.then(function(packageList) {
|
||||
var p = packageList.packageWithName(packageName);
|
||||
if (!p) return;
|
||||
this.package(p);
|
||||
this.downloadsPercentile(packageList.downloadsPercentileForPackage(p));
|
||||
this.neededBy(packageList.dependenciesOnPackageName(packageName));
|
||||
this.packageWithName = packageList.packageWithName;
|
||||
m.request({method: "GET",
|
||||
url: p.readmeURL(),
|
||||
deserialize: function(v){return v;}
|
||||
}).then(this.readme);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
melpa.packagedetails.view = function(ctrl) {
|
||||
var pkg = ctrl.package();
|
||||
if (!pkg) return m("h1", "Package not found");
|
||||
this.depLink = function(dep) {
|
||||
var depPkg = ctrl.packageWithName(dep.name);
|
||||
var label = dep.name + " " + dep.version;
|
||||
return depPkg ? m("a", {href: "/" + dep.name, config: m.route}, label) : label;
|
||||
};
|
||||
this.packageLink = function(pkg) {
|
||||
return m("a", {href: "/" + pkg.name(), config: m.route}, pkg.name());
|
||||
};
|
||||
return m("section", [
|
||||
m("h1", [
|
||||
pkg.name(),
|
||||
" ",
|
||||
m("small", pkg.version())
|
||||
]),
|
||||
m("p.lead", pkg.description()),
|
||||
m("p", [
|
||||
m("a.btn.btn-default", {href: pkg.recipeURL()}, [glyphicon('cutlery'), " Recipe"]), ' ',
|
||||
m("a.btn.btn-default", {href: pkg.packageURL()}, [glyphicon('download'), " Download"]), ' ',
|
||||
(pkg.sourceURL() ? m("a.btn.btn-default", {href: pkg.sourceURL()}, [glyphicon('home'), " Homepage"]) : '')
|
||||
]),
|
||||
m("section", [
|
||||
m(".well", [
|
||||
m("dl.dl-horizontal", _.flatten([
|
||||
m("dt", "Downloads"),
|
||||
m("dd", [
|
||||
pkg.downloads(),
|
||||
m("span.muted", " (all versions)"),
|
||||
", percentile: ",
|
||||
ctrl.downloadsPercentile().toFixed(2)
|
||||
]),
|
||||
m("dt", "Source"),
|
||||
m("dd", [
|
||||
pkg.sourceURL() ? m("a", {href: pkg.sourceURL()}, pkg.source()) : m("span", pkg.source())
|
||||
]),
|
||||
m("dt", "Dependencies"),
|
||||
m("dd", intersperse(pkg.dependencies().map(this.depLink), " / ")),
|
||||
m("dt", "Needed by"),
|
||||
m("dd", intersperse(ctrl.neededBy().map(this.packageLink), " / ")),
|
||||
pkg.oldNames().length > 0 ? _.flatten([
|
||||
m("dt", "Renamed from:"),
|
||||
pkg.oldNames()
|
||||
// m("dt", "Old name needed by:"),
|
||||
// m("dd", "TODO")
|
||||
]) : []
|
||||
]))
|
||||
])
|
||||
]),
|
||||
m("section", [
|
||||
m("h4", "Description"),
|
||||
m("pre", ctrl.readme())
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Showing last build time
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
melpa.buildstatus = {};
|
||||
melpa.buildstatus.controller = function() {
|
||||
this.buildCompletionTime = m.request({method: 'GET', url: "/build-status.json"})
|
||||
.then(function(status){
|
||||
return new Date(status.completed * 1000);
|
||||
});
|
||||
};
|
||||
melpa.buildstatus.view = function(ctrl) {
|
||||
return m(".alert.alert-success", [
|
||||
m("strong", "Last build ended: "),
|
||||
m("span", [moment(ctrl.buildCompletionTime()).fromNow()])
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Static pages
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
melpa.staticpage = function(partialPath) {
|
||||
this.controller = function() {
|
||||
this.content = m.prop('');
|
||||
m.request({method: "GET", url: partialPath,
|
||||
deserialize: function(v){return v;}
|
||||
}).then(this.content);
|
||||
};
|
||||
this.view = function(ctrl) {
|
||||
return m("div", [m.trust(ctrl.content())]);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Front page
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
melpa.frontpage = {};
|
||||
melpa.frontpage.controller = function() {
|
||||
this.packagelist = new melpa.packagelist.controller();
|
||||
this.buildstatus = new melpa.buildstatus.controller();
|
||||
};
|
||||
melpa.frontpage.view = function(ctrl) {
|
||||
return m("div", [
|
||||
m("section.page-header", [
|
||||
m("h1", [
|
||||
"MELPA",
|
||||
m("small", " (Milkypostman’s Emacs Lisp Package Archive)")
|
||||
])
|
||||
]),
|
||||
m(".row", [
|
||||
m(".col-md-8", [
|
||||
m("section.jumbotron", [
|
||||
m("ul", [
|
||||
m("li", m.trust("<strong>Up-to-date packages built on our servers from upstream source</strong>")),
|
||||
m("li", m.trust("<strong>Installable in any recent Emacs using 'package.el'</strong> - no need to install svn/cvs/hg/bzr/git/darcs etc.")),
|
||||
m("li", m.trust("<strong>Curated</strong> - no obsolete, renamed, forked or randomly hacked packages")),
|
||||
m("li", m.trust("<strong>Comprehensive</strong> - more packages than any other archive")),
|
||||
m("li", m.trust("<strong>Automatic updates</strong> - new commits result in new packages")),
|
||||
m("li", m.trust("<strong>Extensible</strong> - contribute recipes via github, and we'll build the packages"))
|
||||
])
|
||||
])
|
||||
]),
|
||||
m(".col-md-4", [
|
||||
melpa.buildstatus.view(ctrl.buildstatus),
|
||||
m.trust('<a class="twitter-timeline" data-dnt="true" data-related="milkypostman,sanityinc" href="https://twitter.com/melpa_emacs" data-widget-id="311867756586864640">Tweets by @melpa_emacs</a>')
|
||||
])
|
||||
]),
|
||||
melpa.packagelist.view(ctrl.packagelist)
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Routing
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
melpa.gettingstarted = new melpa.staticpage("/partials/getting-started.html");
|
||||
|
||||
m.route.mode = "hash";
|
||||
m.route(document.getElementById("content"), "/", {
|
||||
"/": melpa.frontpage,
|
||||
"!/": melpa.frontpage, // Synonym for backward-compatibility
|
||||
"/getting-started": melpa.gettingstarted,
|
||||
"/:package": melpa.packagedetails,
|
||||
"!/:package": melpa.packagedetails // Synonym for backward-compatibility
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// 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);
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Lazily initialise twitter widgets as they appear
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
window.setInterval(function() {
|
||||
if (window.twttr && window.twttr.widgets) window.twttr.widgets.load();
|
||||
}, 100);
|
||||
|
||||
})(window.m, window.document, window._, window.moment);
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<section>
|
||||
<a name="packages"></a>
|
||||
<h2>Current List of <span ng-bind="packages.length"></span> Packages
|
||||
<small ng-show="totalDownloads">(<span ng-bind="totalDownloads | number"></span> downloads to date)</small>
|
||||
</h2>
|
||||
<p>
|
||||
<input type="text" ng-model="enteredSearchTerms" debounce-delay="250" placeholder="Enter filter terms" autofocus>
|
||||
<span class="muted"> <ng-pluralize when="{1: '1 match', 'other': '{} matches'}" count="(packages | filter:packageMatcher(searchTerms)).length"/></span>
|
||||
</p>
|
||||
<table id="package-list" class="table table-bordered table-responsive table-hover">
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th table-sort="orderBy" sort-field="name">Package</th>
|
||||
<th table-sort="orderBy" sort-field="description">Description</th>
|
||||
<th table-sort="orderBy" sort-field="version">Version</th>
|
||||
<th>Recipe</th>
|
||||
<th table-sort="orderBy" sort-field="fetcher">Source</th>
|
||||
<th table-sort="orderBy" sort-field="downloads">DLs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="package in packages | filter:packageMatcher(searchTerms) | orderBy:orderBy">
|
||||
<td><a href="#/{{package.name}}">{{package.name}}</a></td>
|
||||
<td><a href="#/{{package.name}}">{{package.description}}</a></td>
|
||||
<td class="version"><a href="{{package.packageURL}}">{{package.version}} <span class="glyphicon glyphicon-download"></span></a></td>
|
||||
<td class="recipe"><a href="{{package.recipeURL}}"><span class="glyphicon glyphicon-cutlery"></span></a></td>
|
||||
<td class="source">
|
||||
<a ng-if="package.sourceURL" href="{{package.sourceURL}}">{{package.source}}</a>
|
||||
<span ng-if="!package.sourceURL">{{package.source}}</span>
|
||||
</td>
|
||||
<td>{{package.downloads}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
|
@ -1,49 +0,0 @@
|
|||
<h1>{{pkg.name}}
|
||||
<small>{{pkg.version}}</small>
|
||||
</h1>
|
||||
|
||||
<p class="lead">{{pkg.description}}</p>
|
||||
<p>
|
||||
<a href="{{pkg.recipeURL}}" class="btn btn-default"><span class="glyphicon glyphicon-cutlery"> Recipe</a>
|
||||
<a href="{{pkg.packageURL}}" class="btn btn-default"><span class="glyphicon glyphicon-download"> Download</a>
|
||||
<a ng-if="pkg.sourceURL" href="{{pkg.sourceURL}}" class="btn btn-default"><span class="glyphicon glyphicon-home"> Homepage</a>
|
||||
</p>
|
||||
<section>
|
||||
<div class="well">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Downloads</dt>
|
||||
<dd>{{pkg.downloads}} <span class="muted">(all versions)</span>, percentile: {{downloadsPercentile | number:2}}</dd>
|
||||
<dt>Source</dt>
|
||||
<dd>
|
||||
<a ng-show="pkg.sourceURL" href="{{pkg.sourceURL}}">{{pkg.source}}</a>
|
||||
<span ng-show="!pkg.sourceURL">{{pkg.source}}</span>
|
||||
</dd>
|
||||
<dt>Dependencies</dt>
|
||||
<dd><span ng-show="pkg.dependencies.length > 0" ng-repeat="dep in pkg.dependencies">
|
||||
<a ng-if="havePackage(dep.name)" href="#/{{dep.name}}">{{dep.name}} {{dep.version}}</a>
|
||||
<span ng-if="!havePackage(dep.name)">{{dep.name}} {{dep.version}}</span>
|
||||
<span class="muted" ng-if="!$last">/</span></span>
|
||||
<span class="muted" ng-show="pkg.dependencies.length == 0">-</span>
|
||||
</dd>
|
||||
<dt>Needed by</dt>
|
||||
<dd><span ng-show="reverseDependencies.length > 0" ng-repeat="dep in reverseDependencies | orderBy:'name'">
|
||||
<a href="#/{{dep.name}}">{{dep.name}}</a>
|
||||
<span class="muted" ng-if="!$last">/</span></span>
|
||||
<span class="muted" ng-show="reverseDependencies.length == 0">-</span>
|
||||
</dd>
|
||||
<dt ng-if="pkg.oldNames.length > 0">Renamed from:<dt>
|
||||
<dd ng-if="pkg.oldNames.length > 0"><span ng-repeat="oldName in pkg.oldNames"><span class="muted" ng-if="!$first">,</span> {{oldName}}</span></dd>
|
||||
<dt ng-if="reverseDependenciesOldNames.length > 0">Old name needed by</dt>
|
||||
<dd ng-if="reverseDependenciesOldNames.length > 0">
|
||||
<span ng-repeat="dep in reverseDependenciesOldNames | orderBy:'name'">
|
||||
<a href="#/{{dep.name}}">{{dep.name}}</a>
|
||||
<span class="muted" ng-if="!$last">/</span></span>
|
||||
<span class="muted" ng-show="reverseDependenciesOldNames.length == 0">-</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h4>Description</h4>
|
||||
<pre>{{readme}}</pre>
|
||||
</section>
|
Loading…
Reference in a new issue