From 780c6e605525dddadab2a36a6709b141fa8fcba0 Mon Sep 17 00:00:00 2001
From: Leighton Whiting <wild.katana@gmail.com>
Date: Thu, 15 Nov 2012 12:19:35 -0700
Subject: [PATCH] Added project browser to core

---
 .../project_browser/css/jquery.multiselect.css     |   23 +
 .../project_browser/css/project_browser.css        |  203 +++++
 core/modules/project_browser/images/arrow-asc.png  |    5 +
 core/modules/project_browser/images/arrow-desc.png |    4 +
 core/modules/project_browser/images/circle.png     |    3 +
 core/modules/project_browser/images/red-x.png      |    7 +
 .../project_browser/js/jquery.multiselect.min.js   |   44 +
 .../js/project_browser_categories_widget.js        |   11 +
 .../js/project_browser_more_link.js                |   28 +
 .../js/project_browser_multiselect.js              |   13 +
 core/modules/project_browser/js/select_releases.js |   12 +
 .../project_browser/project_browser.admin.inc      |   32 +
 core/modules/project_browser/project_browser.inc   |  880 ++++++++++++++++++++
 core/modules/project_browser/project_browser.info  |    7 +
 .../modules/project_browser/project_browser.module |  467 +++++++++++
 .../project_browser/project_browser.pages.inc      |  521 ++++++++++++
 core/modules/project_browser/project_browser.test  |   75 ++
 .../tests/project_browser_test.info                |   12 +
 .../tests/project_browser_test.module              |  329 ++++++++
 .../theme/project-browser-block.tpl.php            |   21 +
 .../theme/project-browser-install-queue.tpl.php    |   16 +
 .../theme/project-browser-install.tpl.php          |   21 +
 .../theme/project-browser-list.tpl.php             |   26 +
 .../theme/project-browser-project.tpl.php          |   59 ++
 .../theme/project_browser.admin.inc                |   32 +
 25 files changed, 2851 insertions(+), 0 deletions(-)
 create mode 100644 core/modules/project_browser/css/jquery.multiselect.css
 create mode 100644 core/modules/project_browser/css/project_browser.css
 create mode 100644 core/modules/project_browser/images/arrow-asc.png
 create mode 100644 core/modules/project_browser/images/arrow-desc.png
 create mode 100644 core/modules/project_browser/images/circle.png
 create mode 100644 core/modules/project_browser/images/red-x.png
 create mode 100644 core/modules/project_browser/js/jquery.multiselect.min.js
 create mode 100644 core/modules/project_browser/js/project_browser_categories_widget.js
 create mode 100644 core/modules/project_browser/js/project_browser_more_link.js
 create mode 100644 core/modules/project_browser/js/project_browser_multiselect.js
 create mode 100644 core/modules/project_browser/js/select_releases.js
 create mode 100644 core/modules/project_browser/project_browser.admin.inc
 create mode 100644 core/modules/project_browser/project_browser.inc
 create mode 100644 core/modules/project_browser/project_browser.info
 create mode 100644 core/modules/project_browser/project_browser.module
 create mode 100644 core/modules/project_browser/project_browser.pages.inc
 create mode 100644 core/modules/project_browser/project_browser.test
 create mode 100644 core/modules/project_browser/tests/project_browser_test.info
 create mode 100644 core/modules/project_browser/tests/project_browser_test.module
 create mode 100644 core/modules/project_browser/theme/project-browser-block.tpl.php
 create mode 100644 core/modules/project_browser/theme/project-browser-install-queue.tpl.php
 create mode 100644 core/modules/project_browser/theme/project-browser-install.tpl.php
 create mode 100644 core/modules/project_browser/theme/project-browser-list.tpl.php
 create mode 100644 core/modules/project_browser/theme/project-browser-project.tpl.php
 create mode 100644 core/modules/project_browser/theme/project_browser.admin.inc

diff --git a/core/modules/project_browser/css/jquery.multiselect.css b/core/modules/project_browser/css/jquery.multiselect.css
new file mode 100644
index 0000000..e8f8e00
--- /dev/null
+++ b/core/modules/project_browser/css/jquery.multiselect.css
@@ -0,0 +1,23 @@
+.ui-multiselect { padding:2px 0 2px 4px; text-align:left }
+.ui-multiselect span.ui-icon { float:right }
+.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; }
+.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important }
+
+.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px }
+.ui-multiselect-header ul { font-size:0.9em }
+.ui-multiselect-header ul li { float:left; padding:0 10px 0 0; list-style: none; }
+.ui-multiselect-header a { text-decoration:none }
+.ui-multiselect-header a:hover { text-decoration:underline }
+.ui-multiselect-header span.ui-icon { float:left }
+.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 }
+
+.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; width: 500px; }
+.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:scroll }
+.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px }
+.ui-multiselect-checkboxes label input { position:relative; top:1px; margin-right: 5px; }
+.ui-multiselect-checkboxes li { float:left; width: 225px; font-size:0.9em; padding-right:3px; list-style: none; }
+.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid }
+.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none }
+
+/* remove label borders in IE6 because IE6 does not support transparency */
+* html .ui-multiselect-checkboxes label { border:none }
diff --git a/core/modules/project_browser/css/project_browser.css b/core/modules/project_browser/css/project_browser.css
new file mode 100644
index 0000000..68b9b75
--- /dev/null
+++ b/core/modules/project_browser/css/project_browser.css
@@ -0,0 +1,203 @@
+#project-browser-install-button-form {
+  clear: both;
+  margin: 10px 0 0;
+}
+
+#project-browser-install-queue {
+  margin: 5px 0 0;
+}
+
+.project-browser-install-queue-item {
+  margin: 5px 0;
+}
+
+.project-browser-install-queue-items {
+  clear: both;
+  margin: 0 0 15px;
+}
+
+.project-browser-install-link {
+  clear: both;
+}
+
+.project-browser-install-queue-item a {
+  padding-left: 20px;
+  background: url("../images/red-x.png") no-repeat scroll left 2px transparent;
+}
+
+.project-browser-selected-release {
+  display: none;
+}
+
+.project-browser-show-releases-link {
+  cursor: pointer;
+}
+
+div.item-list ul.project-browser-sort-list, 
+div.item-list ul.project-browser-servers-list {
+  margin: 0;
+}
+
+div.item-list ul.project-browser-sort-list li, 
+div.item-list ul.project-browser-servers-list li {
+  display: inline;
+  list-style-image: none;
+  margin-right: 10px;
+}
+
+div.item-list ul.project-browser-sort-list li a, 
+div.item-list ul.project-browser-servers-list li a {
+  color: #0074BD;
+}
+
+div.item-list ul.project-browser-sort-list li.sort-active a,
+div.item-list ul.project-browser-servers-list li.server-active a {
+  color: #000;
+}
+
+div.item-list ul.project-browser-sort-list li.sort-header,
+div.item-list ul.project-browser-servers-list li.server-header {
+  font-weight: bold;
+}
+
+div.item-list ul.project-browser-sort-list li.sort-asc {
+  background: url("../images/arrow-asc.png") no-repeat scroll right 2px transparent;
+  padding-right: 15px;
+}
+
+div.item-list ul.project-browser-sort-list li.sort-desc {
+  background: url("../images/arrow-desc.png") no-repeat scroll right 2px transparent;
+  padding-right: 15px;
+}
+
+a.show-more {
+  float: right;
+}
+
+.project-extra {
+  clear: both;
+  color: gray;
+  text-align: right;
+}
+
+.project-author {
+  color: gray;
+  font-size: 0.9em;
+}
+
+.project-updated {
+  color: gray;
+  font-size: 0.9em;
+}
+
+.project-image {
+  float: left;
+  margin-right: 10px;
+}
+
+.project-image img {
+  max-height: 150px;
+  max-width: 200px;
+}
+
+#project-browser-main ul, #project-browser-main ol {
+  list-style-position: inside;
+}
+
+.project-item {
+  border-top: 1px solid #E0E0D8;
+  clear: both;
+  padding: 10px;
+  position: relative;
+}
+
+.project-item-first {
+  clear: both;
+  padding: 10px;
+  position: relative;
+}
+
+div.project-status {
+  position: absolute;
+  right: 10px;
+  text-align: right;
+  top: 5px;
+}
+
+div.project-information {
+  
+}
+
+.project-browser-install-main {
+  padding: 10px;
+  float: right;
+  width: 76%; 
+}
+
+.install-disabled {
+  color: gray;
+}
+
+.install-enabled {
+  color: green;
+}
+
+.project-browser-install-sidebar-left {
+  float: left;
+  width: 19%;
+  padding: 10px;
+}
+
+div.project-title {
+  font-size: 18px;
+}
+
+fieldset#edit-category,
+fieldset#edit-version  {
+  border: none;
+}
+
+div.install-item-prefix {
+  float:left;
+  margin-left: 50px;
+}
+
+#project-browser-main div.form-item-install {
+  float: right; 
+}
+
+#project-browser div.project-browser-region {
+  min-height: 1px;
+}
+
+#project-browser div#project-browser-main {
+  width: 75%;
+  float: left;
+  margin-right: 1%;
+}
+
+#project-browser div#project-browser-sidebar-right {
+  width: 23%;
+  float: right;
+}
+
+#project-browser div.project-browser-block {
+  margin-bottom: 20px;
+  border: 1px solid #CCCCCC;
+}
+
+#project-browser .project-browser-region .project-browser-block {
+  clear: both;
+}
+
+#project-browser div.project-browser-block h2 {
+  float: none;
+  font-size: 1em;
+  margin: 0;
+  padding: 3px 10px;
+  background: none repeat scroll 0 0 #E0E0D8;
+}
+
+#project-browser div#project-browser-sidebar-right div.project-browser-block div.content {
+  padding: 5px 10px;
+}
diff --git a/core/modules/project_browser/images/arrow-asc.png b/core/modules/project_browser/images/arrow-asc.png
new file mode 100644
index 0000000..a3ccabc
--- /dev/null
+++ b/core/modules/project_browser/images/arrow-asc.png
@@ -0,0 +1,5 @@
+PNG
+
+   IHDR         H%v?   PLTEkR%   tRNS @f   IDATc`@LLLL
+
+Li |q)    IENDB`
\ No newline at end of file
diff --git a/core/modules/project_browser/images/arrow-desc.png b/core/modules/project_browser/images/arrow-desc.png
new file mode 100644
index 0000000..2edbb17
--- /dev/null
+++ b/core/modules/project_browser/images/arrow-desc.png
@@ -0,0 +1,4 @@
+PNG
+
+   IHDR         H%v?   PLTEkR%   tRNS @f   IDATxc`@0`o``g``
+ KzH    IENDB`
\ No newline at end of file
diff --git a/core/modules/project_browser/images/circle.png b/core/modules/project_browser/images/circle.png
new file mode 100644
index 0000000..6b2d63f
--- /dev/null
+++ b/core/modules/project_browser/images/circle.png
@@ -0,0 +1,3 @@
+PNG
+
+   IHDR         6|J   PLTE   E<   tRNS @f   IDAT[c` cfe ($b       IENDB`
\ No newline at end of file
diff --git a/core/modules/project_browser/images/red-x.png b/core/modules/project_browser/images/red-x.png
new file mode 100644
index 0000000..486390c
--- /dev/null
+++ b/core/modules/project_browser/images/red-x.png
@@ -0,0 +1,7 @@
+PNG
+
+   IHDR         a  IDATxڥ?HBQƣr #p(2hAk
+!GP2ISiъ
+:W_O"sw^gH^3כT(Ppc"31!h>_afEpd!H߯!LNj Mp7<ǃpl2LIlo#6;a!vp˅r4
+u
+RpNE>yHnlG. RWRB?%a<a2#=4}DrOu_	TWXTPjmA(`%xZZjKjFUtǋX'';GWXCW83p~zo$B{;rt AjX)	|!uwsֆhx-=2ةQ-riIMQ(r?6{L~_ͧG    IENDB`
\ No newline at end of file
diff --git a/core/modules/project_browser/js/jquery.multiselect.min.js b/core/modules/project_browser/js/jquery.multiselect.min.js
new file mode 100644
index 0000000..256d8c7
--- /dev/null
+++ b/core/modules/project_browser/js/jquery.multiselect.min.js
@@ -0,0 +1,44 @@
+/*
+ * jQuery MultiSelect UI Widget 1.10a
+ * Modified by Leighton Whiting for Drupal's Project Browser
+ * Copyright (c) 2011 Eric Hynds
+ * 
+ * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
+ * 
+ * Depends:
+ *   - jQuery 1.4.2+
+ *   - jQuery UI 1.8 widget factory
+ *
+ * Optional:
+ *   - jQuery UI effects
+ *   - jQuery UI position utility
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ *   http://www.opensource.org/licenses/mit-license.php
+ *   http://www.gnu.org/licenses/gpl.html
+ *
+ */
+
+(function($,undefined){var multiselectID=0;$.widget("ech.multiselect",{options:{header:true,height:175,minWidth:225,classes:'',checkAllText:'Check all',uncheckAllText:'Uncheck all',noneSelectedText:'Select options',selectedText:'# selected',selectedList:0,show:'',hide:'',autoOpen:false,multiple:true,position:{}},_create:function(){var el=this.element.hide(),o=this.options;this.speed=$.fx.speeds._default;this._isOpen=false;var
+button=(this.button=$('<button type="button"><span class="ui-icon ui-icon-triangle-2-n-s"></span></button>')).addClass('ui-multiselect ui-widget ui-state-default ui-corner-all').addClass(o.classes).attr({'title':el.attr('title'),'aria-haspopup':true,'tabIndex':el.attr('tabIndex')}).insertAfter(el),buttonlabel=(this.buttonlabel=$('<span />')).html(o.noneSelectedText).appendTo(button),menu=(this.menu=$('<div />')).addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all').addClass(o.classes).insertAfter(button),header=(this.header=$('<div />')).addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix').appendTo(menu),headerLinkContainer=(this.headerLinkContainer=$('<ul />')).addClass('ui-helper-reset').html(function(){if(o.header===true){return'<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>'+o.checkAllText+'</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>'+o.uncheckAllText+'</span></a></li>';}else if(typeof o.header==="string"){return'<li>'+o.header+'</li>';}else{return'';}}).append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>').appendTo(header),checkboxContainer=(this.checkboxContainer=$('<ul />')).addClass('ui-multiselect-checkboxes ui-helper-reset').appendTo(menu);this._bindEvents();this.refresh(true);if(!o.multiple){menu.addClass('ui-multiselect-single');}},_init:function(){if(this.options.header===false){this.header.hide();}
+if(!this.options.multiple){this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide();}
+if(this.options.autoOpen){this.open();}
+if(this.element.is(':disabled')){this.disable();}},refresh:function(init){var el=this.element,o=this.options,menu=this.menu,checkboxContainer=this.checkboxContainer,optgroups=[],html=[],id=el.attr('id')||multiselectID++;this.element.find('option').each(function(i){var $this=$(this),parent=this.parentNode,title=this.innerHTML,value=this.value,inputID=this.id||'ui-multiselect-'+id+'-option-'+i,isDisabled=this.disabled,isSelected=this.selected,labelClasses=['ui-corner-all'],optLabel;if(parent.tagName.toLowerCase()==='optgroup'){optLabel=parent.getAttribute('label');if($.inArray(optLabel,optgroups)===-1){html.push('<li class="ui-multiselect-optgroup-label"><a href="#">'+optLabel+'</a></li>');optgroups.push(optLabel);}}
+if(isDisabled){labelClasses.push('ui-state-disabled');}
+if(isSelected&&!o.multiple){labelClasses.push('ui-state-active');}
+html.push('<li class="'+(isDisabled?'ui-multiselect-disabled':'')+'">');html.push('<label for="'+inputID+'" class="'+labelClasses.join(' ')+'">');html.push('<input id="'+inputID+'" name="multiselect_'+id+'" type="'+(o.multiple?"checkbox":"radio")+'" value="'+value+'" title="'+title+'"');if(isSelected){html.push(' checked="checked"');html.push(' aria-selected="true"');}
+if(isDisabled){html.push(' disabled="disabled"');html.push(' aria-disabled="true"');}
+html.push(' /><span>'+title+'</span></label></li>');});checkboxContainer.html(html.join(''));this.labels=menu.find('label');this._setButtonWidth();this._setMenuWidth();this.button[0].defaultValue=this.update();if(!init){this._trigger('refresh');}},update:function(){var o=this.options,$inputs=this.labels.find('input'),$checked=$inputs.filter(':checked'),numChecked=$checked.length,value;if(numChecked===0){value=o.noneSelectedText;}else{if($.isFunction(o.selectedText)){value=o.selectedText.call(this,numChecked,$inputs.length,$checked.get());}else if(/\d/.test(o.selectedList)&&o.selectedList>0&&numChecked<=o.selectedList){value=$checked.map(function(){return this.title;}).get().join(', ');}else{value=o.selectedText.replace('#',numChecked).replace('#',$inputs.length);}}
+this.buttonlabel.html(value);return value;},_bindEvents:function(){var self=this,button=this.button;function clickHandler(){self[self._isOpen?'close':'open']();return false;}
+button.find('span').bind('click.multiselect',clickHandler);button.bind({click:clickHandler,keypress:function(e){switch(e.which){case 27:case 38:case 37:self.close();break;case 39:case 40:self.open();break;}},mouseenter:function(){if(!button.hasClass('ui-state-disabled')){$(this).addClass('ui-state-hover');}},mouseleave:function(){$(this).removeClass('ui-state-hover');},focus:function(){if(!button.hasClass('ui-state-disabled')){$(this).addClass('ui-state-focus');}},blur:function(){$(this).removeClass('ui-state-focus');}});this.header.delegate('a','click.multiselect',function(e){if($(this).hasClass('ui-multiselect-close')){self.close();}else{self[$(this).hasClass('ui-multiselect-all')?'checkAll':'uncheckAll']();}
+e.preventDefault();});this.menu.delegate('li.ui-multiselect-optgroup-label a','click.multiselect',function(e){e.preventDefault();var $this=$(this),$inputs=$this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'),nodes=$inputs.get(),label=$this.parent().text();if(self._trigger('beforeoptgrouptoggle',e,{inputs:nodes,label:label})===false){return;}
+self._toggleChecked($inputs.filter(':checked').length!==$inputs.length,$inputs);self._trigger('optgrouptoggle',e,{inputs:nodes,label:label,checked:nodes[0].checked});}).delegate('label','mouseenter.multiselect',function(){if(!$(this).hasClass('ui-state-disabled')){self.labels.removeClass('ui-state-hover');$(this).addClass('ui-state-hover').find('input').focus();}}).delegate('label','keydown.multiselect',function(e){e.preventDefault();switch(e.which){case 9:case 27:self.close();break;case 38:case 40:case 37:case 39:self._traverse(e.which,this);break;case 13:$(this).find('input')[0].click();break;}}).delegate('input[type="checkbox"], input[type="radio"]','click.multiselect',function(e){var $this=$(this),val=this.value,checked=this.checked,tags=self.element.find('option');if(this.disabled||self._trigger('click',e,{value:val,text:this.title,checked:checked})===false){e.preventDefault();return;}
+$this.attr('aria-selected',checked);tags.each(function(){if(this.value===val){this.selected=checked;}else if(!self.options.multiple){this.selected=false;}});if(!self.options.multiple){self.labels.removeClass('ui-state-active');$this.closest('label').toggleClass('ui-state-active',checked);self.close();}
+setTimeout($.proxy(self.update,self),10);});$(document).bind('mousedown.multiselect',function(e){if(self._isOpen&&!$.contains(self.menu[0],e.target)&&!$.contains(self.button[0],e.target)&&e.target!==self.button[0]){self.close();}});$(this.element[0].form).bind('reset.multiselect',function(){setTimeout(function(){self.update();},10);});},_setButtonWidth:function(){var width=this.element.outerWidth(),o=this.options;this.button.width(width);},_setMenuWidth:function(){var m=this.menu,width=this.element.outerWidth(),o=this.options;if(/\d/.test(o.minWidth)&&width<o.minWidth){width=o.minWidth;}
+m.width(width||this.button.outerWidth());},_traverse:function(which,start){var $start=$(start),moveToLast=which===38||which===37,$next=$start.parent()[moveToLast?'prevAll':'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)')[moveToLast?'last':'first']();if(!$next.length){var $container=this.menu.find('ul:last');this.menu.find('label')[moveToLast?'last':'first']().trigger('mouseover');$container.scrollTop(moveToLast?$container.height():0);}else{$next.find('label').trigger('mouseover');}},_toggleCheckbox:function(prop,flag){return function(){!this.disabled&&(this[prop]=flag);if(flag){this.setAttribute('aria-selected',true);}else{this.removeAttribute('aria-selected');}}},_toggleChecked:function(flag,group){var $inputs=(group&&group.length)?group:this.labels.find('input'),self=this;$inputs.each(this._toggleCheckbox('checked',flag));this.update();var values=$inputs.map(function(){return this.value;}).get();this.element.find('option').each(function(){if(!this.disabled&&$.inArray(this.value,values)>-1){self._toggleCheckbox('selected',flag).call(this);}});},_toggleDisabled:function(flag){this.button.attr({'disabled':flag,'aria-disabled':flag})[flag?'addClass':'removeClass']('ui-state-disabled');this.menu.find('input').attr({'disabled':flag,'aria-disabled':flag}).parent()[flag?'addClass':'removeClass']('ui-state-disabled');this.element.attr({'disabled':flag,'aria-disabled':flag});},open:function(e){var self=this,button=this.button,menu=this.menu,speed=this.speed,o=this.options;if(this._trigger('beforeopen')===false||button.hasClass('ui-state-disabled')||this._isOpen){return;}
+var $container=menu.find('ul:last'),effect=o.show,pos=button.position();if($.isArray(o.show)){effect=o.show[0];speed=o.show[1]||self.speed;}
+$container.scrollTop(0).height(o.height);if($.ui.position&&!$.isEmptyObject(o.position)){o.position.of=o.position.of||button;menu.show().position(o.position).hide().show(effect,speed);}else{menu.css({top:pos.top+button.outerHeight(),left:pos.left}).show(effect,speed);}
+this.labels.eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus');button.addClass('ui-state-active');this._isOpen=true;this._trigger('open');},close:function(){if(this._trigger('beforeclose')===false){return;}
+var o=this.options,effect=o.hide,speed=this.speed;if($.isArray(o.hide)){effect=o.hide[0];speed=o.hide[1]||this.speed;}
+this.menu.hide(effect,speed);this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave');this._isOpen=false;this._trigger('close');},enable:function(){this._toggleDisabled(false);},disable:function(){this._toggleDisabled(true);},checkAll:function(e){this._toggleChecked(true);this._trigger('checkAll');},uncheckAll:function(){this._toggleChecked(false);this._trigger('uncheckAll');},getChecked:function(){return this.menu.find('input').filter(':checked');},destroy:function(){$.Widget.prototype.destroy.call(this);this.button.remove();this.menu.remove();this.element.show();return this;},isOpen:function(){return this._isOpen;},widget:function(){return this.menu;},_setOption:function(key,value){var menu=this.menu;switch(key){case'header':menu.find('div.ui-multiselect-header')[value?'show':'hide']();break;case'checkAllText':menu.find('a.ui-multiselect-all span').eq(-1).text(value);break;case'uncheckAllText':menu.find('a.ui-multiselect-none span').eq(-1).text(value);break;case'height':menu.find('ul:last').height(parseInt(value,10));break;case'minWidth':this.options[key]=parseInt(value,10);this._setButtonWidth();this._setMenuWidth();break;case'selectedText':case'selectedList':case'noneSelectedText':this.options[key]=value;this.update();break;case'classes':menu.add(this.button).removeClass(this.options.classes).addClass(value);break;}
+$.Widget.prototype._setOption.apply(this,arguments);}});})(jQuery);
\ No newline at end of file
diff --git a/core/modules/project_browser/js/project_browser_categories_widget.js b/core/modules/project_browser/js/project_browser_categories_widget.js
new file mode 100644
index 0000000..c28f738
--- /dev/null
+++ b/core/modules/project_browser/js/project_browser_categories_widget.js
@@ -0,0 +1,11 @@
+(function ($) {
+  $('#edit-categories').multiselect({
+    noneSelectedText: '" . t('Choose') . "...',
+    selectedList: 99,
+    minWidth: 500,
+    position: {
+      my: 'right top',
+      at: 'right bottom'
+    }
+  });
+})(jQuery);
\ No newline at end of file
diff --git a/core/modules/project_browser/js/project_browser_more_link.js b/core/modules/project_browser/js/project_browser_more_link.js
new file mode 100644
index 0000000..07dfc04
--- /dev/null
+++ b/core/modules/project_browser/js/project_browser_more_link.js
@@ -0,0 +1,28 @@
+(function ($) {
+  $(document).ready(function() {
+    // The height of the content block when it's not expanded
+    var adjustheight = 80;
+    // The "more" link text
+    var moreText = "More";
+    // The "less" link text
+    var lessText = "Less";
+    
+    $(".project-information .project-description").each(function(index) {
+      if ($(this).height() > adjustheight)
+      {
+        $(this).css('height', adjustheight).css('overflow', 'hidden');
+        $(this).parents(".project-information").append('<a href="#" class="show-more"></a>');
+      }
+    });
+    
+    $("a.show-more").text(moreText);
+    
+    $(".show-more").toggle(function() {
+        $(this).parents("div:first").find(".project-description").css('height', 'auto').css('overflow', 'visible');
+        $(this).text(lessText);
+      }, function() {
+        $(this).parents("div:first").find(".project-description").css('height', adjustheight).css('overflow', 'hidden');
+        $(this).text(moreText);
+    });
+  });
+})(jQuery);
diff --git a/core/modules/project_browser/js/project_browser_multiselect.js b/core/modules/project_browser/js/project_browser_multiselect.js
new file mode 100644
index 0000000..bf1f49e
--- /dev/null
+++ b/core/modules/project_browser/js/project_browser_multiselect.js
@@ -0,0 +1,13 @@
+(function ($) {
+  $(document).ready(function() {
+    $('#edit-categories').multiselect({
+      noneSelectedText: Drupal.t("Choose"),
+      selectedList: 99,
+      minWidth: 500,
+      position: {
+        my: 'right top',
+        at: 'right bottom'
+      }
+    });
+  });
+})(jQuery);
diff --git a/core/modules/project_browser/js/select_releases.js b/core/modules/project_browser/js/select_releases.js
new file mode 100644
index 0000000..97cc780
--- /dev/null
+++ b/core/modules/project_browser/js/select_releases.js
@@ -0,0 +1,12 @@
+(function ($) {
+  $(document).ready(function() {
+    $('.project-browser-releases-wrapper').hide();
+    $('.project-browser-selected-release').show();
+    
+    $('.project-browser-show-releases-link').click(function() {
+      var target = $(this).attr('rel');
+      $('.project-browser-release-' + target).show();
+      $('.project-browser-selected-release-' + target).hide();
+    })
+  });
+})(jQuery);
diff --git a/core/modules/project_browser/project_browser.admin.inc b/core/modules/project_browser/project_browser.admin.inc
new file mode 100644
index 0000000..831b050
--- /dev/null
+++ b/core/modules/project_browser/project_browser.admin.inc
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @file
+ * Project Browser module admin pages.
+ */
+
+// ======================================
+// Administration Page:
+// ======================================
+
+/**
+ * Admin Settings Form
+ */
+function project_browser_admin() {
+  $form['main'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Main settings'),
+    '#collapsible' => FALSE,
+    '#collapsed' => FALSE,
+  );
+  $form['main']['project_browser_servers'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Repositories'),
+    '#default_value' => variable_get('project_browser_servers', ''),
+    '#description' => t("Add new repositories to use for the Project Browser, one per line, in 
+      the 'url|method|Site Name' format. Drupal.org is added by default, and doesn't need to be 
+      set here."),
+    '#required' => FALSE,
+  );
+
+  return system_settings_form($form);
+}
diff --git a/core/modules/project_browser/project_browser.inc b/core/modules/project_browser/project_browser.inc
new file mode 100644
index 0000000..df7967a
--- /dev/null
+++ b/core/modules/project_browser/project_browser.inc
@@ -0,0 +1,880 @@
+<?php 
+/**
+ * @file
+ * Various functions that are required by project_browser
+ */
+
+use Drupal\Core\Updater\Updater;
+use Drupal\Core\FileTransfer\Local;
+
+/**
+ * Returns the themed install list form
+ */
+function project_browser_get_install_list() {
+  $queued_projects = project_browser_get_queued_projects();
+  
+  // Show a list of the queued projects
+  return theme('project_browser_install_queue', array('projects' => $queued_projects));
+}
+
+/**
+ * Shows an install button for the Install Queue block
+ */
+function project_browser_install_button_form($form, &$form_state) {
+  $form['#attributes']['id'] = 'project-browser-install-button-form';
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 'Install',
+  );
+  $form['#action'] = url('admin/modules/project-browser/install/select_versions');
+  
+  return $form;
+}
+
+/**
+ * Builds the filters form
+ */
+function project_browser_filters_form($form, &$form_state, $type) {
+  $form['search_text'] = array(
+    '#type' => 'textfield',
+    '#size' => '25',
+    '#title' => t('Search String'),
+    '#default_value' => isset($_SESSION['project_browser_text_filter_' . $type]) ? $_SESSION['project_browser_text_filter_' . $type] : '',
+  );
+  
+  // Add the categories filter if there are categories
+  if ($categories = project_browser_get_categories($type)) {
+    $form['categories'] = array(
+      '#type' => 'select',
+      '#title' => t('Categories'),
+      '#multiple' => TRUE,
+      '#options' => $categories,
+      '#prefix' => '<div id="project-browser-filters-categories">',
+      '#suffix' => '</div>',
+      '#default_value' => isset($_SESSION['project_browser_category_filter_' . $type]) ? $_SESSION['project_browser_category_filter_' . $type] : array(),
+    );
+  }
+  
+  $form['project_type'] = array(
+    '#type' => 'value',
+    '#value' => $type,
+  );
+  
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Filter'),
+  );
+  
+  return $form;
+}
+
+/**
+ * Returns a themed sort widget for the filters
+ */
+function project_browser_get_sort_widget($sort_options, $current_order_by, $current_sort) {
+  $sort_list = array();
+  $sort_list[] = array('data' => t('Sort by:'), 'class' => array('sort-header'));
+  $current_path = drupal_get_path_alias(current_path());
+  
+  foreach ($sort_options as $sort_option) {
+    $classes = array();
+    $query = array(
+      'order_by' => $sort_option['method'],
+      'sort' => $sort_option['default_sort'],
+    );
+    
+    // If the sort option is currently active, handle it differently
+    if ($current_order_by == $sort_option['method']) {
+      $classes[] = 'sort-active';
+      $classes[] = 'sort-' . $current_sort;
+      
+      // Set the direction of the sort link to the opposite of what it currently is
+      if ($current_sort == $query['sort']) {
+        if ($query['sort'] == 'desc') {
+          $query['sort'] = 'asc';
+        }
+        else {
+          $query['sort'] = 'desc';
+        }
+      }
+    }
+    else {
+      $classes[] = 'sort-inactive';
+    }
+    
+    $sort_list[] = array(
+      'data' => l($sort_option['name'], $current_path, array('query' => $query, 'class' => array())),
+      'class' => $classes,
+    );
+  }
+
+  return theme('item_list', array(
+      'items' => $sort_list, 
+      'type' => 'ul',
+      'attributes' => array('class' => array('project-browser-sort-list'))));
+}
+
+/**
+ * Returns a themed sort widget for the filters
+ */
+function project_browser_get_server_widget($servers, $current_server) {
+  $list = array();
+  $list[] = array('data' => t('Repository:'), 'class' => array('server-header'));
+  $current_path = drupal_get_path_alias($_GET['q']);
+  
+  $i = 0;
+  
+  foreach ($servers as $url => $server) {
+    $classes = array();
+    $query = array(
+      'repository' => $i,
+    );
+    
+    // If the sort option is currently active, handle it differently
+    if ($current_server == $i) {
+      $classes[] = 'server-active';
+    }
+    else {
+      $classes[] = 'server-inactive';
+    }
+    
+    $list[] = array(
+      'data' => l($server['name'], $current_path, array('query' => $query, 'class' => array())),
+      'class' => $classes,
+    );
+    
+    $i += 1;
+  }
+
+  return theme('item_list', array(
+      'items' => $list, 
+      'type' => 'ul',
+      'attributes' => array('class' => array('project-browser-servers-list'))));
+}
+
+
+/**
+ * Returns a list of sort options
+ * 
+ * @param $full
+ *   Set this to TRUE if you want to get all of the supported sort methods
+ */
+function project_browser_get_sort_options($full = FALSE) {
+  $sort_options = array(
+  	'score' => array('method' => 'score', 'name' => t('Relevancy'), 'default_sort' => 'desc'),
+    'usage' => array('method' => 'usage', 'name' => t('Most installed'), 'default_sort' => 'desc'),
+    'title' => array('method' => 'title', 'name' => t('Title'), 'default_sort' => 'asc'),
+    'name' => array('method' => 'name', 'name' => t('Author'), 'default_sort' => 'asc'),
+    'latest_release' => array('method' => 'latest_release', 'name' => t('Latest release'), 'default_sort' => 'desc'),
+  );
+  
+  if ($full) {
+  	$sort_options['type'] = array('method' => 'type', 'name' => t('Type'), 'default_sort' => 'asc');
+    $sort_options['created'] = array('method' => 'created', 'name' => t('Date created'), 'default_sort' => 'asc');
+    $sort_options['latest_activity'] = array('method' => 'latest_activity', 'name' => t('Latest build'), 'default_sort' => 'desc');
+  }
+  
+  return $sort_options;
+}
+
+/**
+ * Handles the filters form submit
+ */
+function project_browser_filters_form_submit($form, &$form_state) {
+  $type = $form_state['values']['project_type'];
+  if (isset($form_state['values']['categories'])) {
+    $_SESSION['project_browser_category_filter_' . $type] = $form_state['values']['categories'];
+  }
+  else {
+    $_SESSION['project_browser_category_filter_' . $type] = array();
+  }
+  $_SESSION['project_browser_text_filter_' . $type] = $form_state['values']['search_text'];
+}
+
+/**
+ * Returns all available categories for a project type
+ * 
+ * @param $type
+ *   The type of project to get the categories for. Example: 'module' or 'theme'
+ * 
+ * @return
+ *   Array containing all available categories or FALSE if no categories
+ */
+function project_browser_get_categories($type) {
+  $categories = array();
+  
+  // Get the server to use from SESSION
+  if (isset($_SESSION['project_browser_server_filter'])) {
+    $use_server = $_SESSION['project_browser_server_filter'];
+  }
+  else {
+    $use_server = 0;
+  }
+  
+  $categories_raw = project_browser_fetch_categories($type, $use_server);
+
+  if (is_array($categories_raw) AND !empty($categories_raw)) {
+    foreach ($categories_raw as $url => $cats) {
+	    foreach ($cats as $key => $value) {
+	      // Create a new key so that there are no duplicate categories from different sites
+	      $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value));
+	      $categories[$new_key] = $value;
+	    }
+    }
+  }
+  
+  if (is_array($categories) AND !empty($categories)) {
+    ksort($categories);
+    
+    return $categories;
+  }
+  return FALSE;
+}
+
+/**
+ * Prepares the categories for sending to the servers as filters
+ * 
+ * @param $raw_cats
+ *   An array of categories from $form_state['values']
+ * @param $type
+ *   The type of project to prepare the categories for, eg 'module' or 'theme'
+ */
+function project_browser_prepare_categories($raw_cats, $type) {
+  $categories = project_browser_fetch_categories($type);
+  
+  // Set the value of the categories to true if it is selected
+  foreach ($categories as $url => $cats) {
+    foreach ($cats as $key => $value) {
+      $new_key = preg_replace('/[^a-z0-9_]+/', '_', strtolower($value));
+      if (isset($raw_cats[$new_key]) AND $raw_cats[$new_key]) {
+        $categories[$url][$key] = TRUE;
+      }
+      else {
+        unset($categories[$url][$key]);
+      }
+    }
+    
+    // Unset the parent if there are no children
+    if (empty($categories[$url])) {
+      unset($categories[$url]);
+    }
+  }
+  
+  return $categories;
+}
+
+/**
+ * Checks if a project is enabled
+ * 
+ * @param $type
+ *   The type of project. Could be 'theme' or 'module'
+ * @param $name
+ *   The short name of the project
+ * 
+ * @return
+ *   TRUE if the project is enabled, FALSE otherwise
+ */
+function _project_browser_is_project_enabled($type, $name) {
+  switch ($type) {
+    case 'module':
+      return module_exists($name);
+      break;
+    case 'theme':
+      $themes = list_themes();
+      return isset($themes[$name]);
+      break;
+  }
+  return FALSE;
+}
+
+/**
+ * Gets the currently listed projects from the session
+ */
+function project_browser_get_listed_projects() {
+  if (isset($_SESSION['project_browser_listed_projects'])) {
+    return $_SESSION['project_browser_listed_projects'];
+  }
+  
+  return array();
+}
+
+/**
+ * Gets the currently queued projects from the session
+ */
+function project_browser_get_queued_projects($type = NULL) {
+  $projects = array();
+  
+  if (isset($_SESSION['project_browser_install_list'])) {
+    foreach ($_SESSION['project_browser_install_list'] as $project) {
+      if (is_array($project) AND !empty($project)) {
+        if (isset($type) AND $type != $project['type']) {
+          continue;
+        }
+        else {
+          $projects[$project['name']] = $project;
+        }
+      }
+    }
+  }
+  
+  return $projects;
+}
+
+/**
+ * Gets a release from a project and a release_name
+ */
+function project_browser_get_release($release_name, $project) {
+  $release_data = project_browser_get_project_release_data($project);
+  
+  return isset($release_data['releases'][$release_name]) ? $release_data['releases'][$release_name] : FALSE;
+}
+
+/**
+ * Gets the newly installed projects from the session
+ */
+function project_browser_get_installed_projects() {
+  $projects = array();
+  
+  if (isset($_SESSION['project_browser_installed_projects'])) {
+    foreach ($_SESSION['project_browser_installed_projects'] as $project) {
+      if (is_array($project) AND !empty($project)) {
+        $projects[$project['name']] = $project;
+      }
+    }
+  }
+  
+  return $projects;
+}
+
+/**
+ * Adds a project to the install queue
+ */
+function project_browser_install_queue_add($project) {
+  $_SESSION['project_browser_install_list'][$project['name']] = $project;
+}
+
+/**
+ * Removes a project from the install queue
+ */
+function project_browser_install_queue_remove($project_name) {
+	if (isset($_SESSION['project_browser_install_list'][$project_name])) {
+  	unset($_SESSION['project_browser_install_list'][$project_name]);
+	}
+}
+
+/**
+ * Gets the currently queued releases from the session
+ */
+function project_browser_get_queued_releases() {
+  $releases = array();
+  
+  if (isset($_SESSION['project_browser_install_releases_list'])) {
+    foreach ($_SESSION['project_browser_install_releases_list'] as $release_name => $project) {
+      if (is_array($project) AND !empty($project)) {
+        $releases[$release_name] = $project;
+      }
+    }
+  }
+  
+  return $releases;
+}
+
+// ======================================
+// Server Related Functions:
+// ======================================
+
+/**
+ * Fetches results from the servers based on the parameters passed in
+ * 
+ * $filters should be an associative array with the following keys:
+ * array(
+ *   'version' => '7', // The Major Version of Drupal that is running on the Client
+ *   'text' => 'views', // The text that was entered as the search query, or '' if none
+ *   'categories' => array() // The categories that were selected, if any
+ *   'type' => 'module', // The type of project being searched
+ *   'page' => 3, // The zero-based page number
+ *   'requested' => 12, // How many results are requested per page
+ * )
+ * 
+ * The project_browser_fetch_results($filters) call returns an array like this:
+ * 
+ * array(
+ *   'total' = 5, // The total number of results found for the filters
+ *   'projects' => array( // An array of projects returned for this page request
+ *     'views' => array( // A project array keyed by the machine name
+ *       'type' => 'module', // The type of project this is. Can be 'module' or 'theme'
+ *       'title' => 'Views', // The title of the project
+ *       'name' => 'views', // The machine name of the project
+ *       'author' => 'merlinofchaos', // The author's name
+ *       'description' => "Long project description ...",
+ *       'image' => 'http://www.example.com/image.jpg', // Absolute url to the image, if any
+ *       'usage' => '542312', // How many Downloads the module has had
+ *       'project url' => 'http://www.drupal.org/projects/views', // Absolute url to the project page, if any
+ *       'project status url' => 'http://updates.drupal.org/release-history/views/7.x', // The absolute url of the update checker, formatted like how Drupal.org Update Status does it
+ *       'last updated' => '12342523', // UNIX Timestamp of when the project was last updated
+ *       'maintenance status' => 'Actively maintained', // Maintenance status
+ *       'development status' => 'Under active development', // Development status
+ *       'rating' => '9.6', // A rating on a scale of 1 to 10 of the project, if available
+ *       'dependencies' => array( // An array of the dependencies of this module
+ *         'ctools',
+ *       ),
+ *     ),
+ *   'name_2 => array( ... ),
+ *   ),
+ * );
+ * 
+ * @param $filters
+ *   An associative array of queries to use to filter results
+ * 
+ * @return
+ *   Returns an array of results
+ */
+function project_browser_fetch_results($filters) {
+  $servers = project_browser_get_servers($filters['server']);
+  // Attempt to retrieve the cached version of this page
+  $cid = md5(serialize(array_merge($filters, $servers)));
+  
+  if ($cache = cache()->get($cid)) {
+    return $cache->data;
+  }
+  
+  $results = array(
+    'projects' => array(),
+    'total' => 0,
+  );
+  
+  unset($filters['server']);
+  
+  foreach ($servers as $url => $server) {
+    $local_filters = $filters;
+    
+    // We are not using this right now because we only expect to handle 1 server at a time currently
+    // $local_filters['requested'] = floor($filters['requested'] / count($servers));
+    
+    // Send only the relevant categories to the server
+    if (isset($filters['categories'])) {
+      if (!isset($filters['categories'][$url])) {
+        // Don't call a server for results if categories are being used, and none of them belong to the server
+        continue;
+      }
+      $local_filters['categories'] = $filters['categories'][$url];
+    }
+    
+    // Use XMLRPC if it is set
+    if ($server['method'] == 'xmlrpc') {
+      $results_raw = xmlrpc($url, array(
+        'project_browser_server.fetch_results' => array($local_filters),
+      ));
+      
+      // Check for errors
+      if ($error = xmlrpc_error() AND $error->is_error) {
+        drupal_set_message(t("Encountered an error when trying to fetch results from @name. Error @code : @message",
+          array('@name' => $server['name'], '@code' => $error->code, '@message' => $error->message)));
+        continue;
+      }
+    }
+    
+    // Use json if it is set
+    if ($server['method'] == 'json') {
+      $local_filters['method'] = 'query';
+      if (isset($local_filters['categories'])) {
+        $local_filters['categories'] = serialize($local_filters['categories']);
+      }
+			
+			$query_url = $url . '/query/' . $local_filters['type'] . '/8?' . http_build_query($local_filters, FALSE, '&');
+      $response = drupal_http_request($query_url);
+      if ($response->code == '200') {
+        $results_raw = drupal_json_decode($response->data);
+      }
+      else {
+        drupal_set_message(t("Encountered an error when trying to fetch results from @name. Error @code : @message",
+          array('@name' => $server['name'], '@code' => $response->code, '@message' => $response->error)));
+        continue;
+      }
+    }
+    
+    if (isset($results_raw['total'])) {
+      $results['total'] += $results_raw['total'];
+    }
+    
+    if (isset($results_raw['projects']) AND !empty($results_raw['projects'])) {
+      // Merge the results
+      $results['projects'] = array_merge($results['projects'], $results_raw['projects']);
+    }
+  }
+
+  // Set the cached version of the results
+  cache()->set($cid, $results, strtotime("+24 hours"));
+  
+  return $results;
+}
+
+/**
+ * Fetches categories from the servers based on the type of project
+ * 
+ * @param $type
+ *   The type of project we are getting categories for
+ * @param $use_server
+ *   (Optional) The server to use. Defaults to 'all'.
+ * 
+ * @return
+ *   Returns an array of the categories
+ */
+function project_browser_fetch_categories($type, $use_server = 'all') {
+  $servers = project_browser_get_servers($use_server);
+  
+  // Attempt to retrieve the cached version of this page
+  $cid = md5('categories_' . $type . serialize($servers));
+  
+  if ($cache = cache()->get($cid)) {
+    return $cache->data;
+  }
+  else {
+    $categories = array();
+    
+    foreach ($servers as $url => $server) {
+      // Use xmlrpc if it is set
+      if ($server['method'] == 'xmlrpc') {
+        $categories_raw = xmlrpc($url, array(
+          'project_browser_server.fetch_categories' => array($type),
+        ));
+        
+        // Check for errors
+        if ($error = xmlrpc_error() AND $error->is_error) {
+          drupal_set_message(t("Encountered an error when trying to fetch categories from @name. Error @code : @message",
+            array('@name' => $server['name'], '@code' => $error->code, '@message' => $error->message)));
+          continue;
+        }
+      }
+      
+      // Use json if it is set
+      if ($server['method'] == 'json') {
+        $params = array(
+          'method' => 'categories',
+          'type' => $type,
+        );
+        $response = drupal_http_request($url . '/categories/' . $type . '?' . http_build_query($params, FALSE, '&'));
+        if ($response->code == '200') {
+          $categories_raw = drupal_json_decode($response->data);
+        }
+        else {
+          drupal_set_message(t("Encountered an error when trying to fetch categories from @name. Error @code : @message",
+            array('@name' => $server['name'], '@code' => $response->code, '@message' => $response->error)));
+          continue;
+        }
+      }
+      
+      if (is_array($categories_raw) AND !empty($categories_raw)) {
+        $categories[$url] = $categories_raw;
+      }
+    }
+    
+    // Cache this for 24 hours
+    cache()->set($cid, $categories, strtotime("+24 hours"));
+  }
+  
+  return $categories;
+}
+
+/**
+ * Gets the servers to use for fetching results
+ * 
+ * @return
+ *   Returns an associative array of servers, populated from the project_browser_servers variable, 
+ *   in 'url => name' format
+ */
+function project_browser_get_servers($use_server = 'all') {
+  // FIXME - Change the link once drupal.org is ready
+  $servers = variable_get('project_browser_default_server', array(
+    'http://drupal:drupal@pbs-drupal_7.redesign.devdrupal.org/project_browser/server' => array(
+      'name' => 'Drupal.org',
+      'method' => 'json',
+    ),
+  ));
+  
+  if ($servers_raw = variable_get('project_browser_servers', '')) {
+    // Process the variable and add the servers to the list
+    $custom_servers = array();
+
+    $list = explode("\n", $servers_raw);
+    $list = array_map('trim', $list);
+    $list = array_filter($list, 'strlen');
+    
+    foreach ($list as $position => $text) {
+      $method = $name = $url = FALSE;
+      
+      $matches = array();
+      if (preg_match('/(.*)\|(.*)\|(.*)/', $text, $matches)) {
+        $url = $matches[1];
+        $method = $matches[2];
+        $name = $matches[3];
+        $custom_servers[$url] = array('name' => $name, 'method' => $method);
+      }
+    }
+    
+    $servers = array_merge($servers, $custom_servers);
+  }
+  
+  // Filter out servers if necessary
+  if ($use_server !== 'all') {
+    $i = 0;
+    foreach ($servers as $url => $server) {
+      if ($use_server != $i) {
+        unset($servers[$url]);
+      }
+      $i += 1;
+    }
+  }
+  
+  return $servers;
+}
+
+/**
+ * Uses the project status url to get the available releases for a project
+ * 
+ * @param $project
+ *   The project to get the releases for
+ * 
+ * @return
+ *   An array of releases for this project
+ */
+function project_browser_get_project_release_data($project) {
+  $releases = array();
+  $project['project_type'] = $project['type'];
+	$project['includes'] = array();
+  
+  // Build the releases cache for this project
+  module_load_include('inc', 'update', 'update.fetch');
+  if (_update_process_fetch_task($project)) {
+    $data = _update_cache_get('available_releases::' . $project['name']);
+    if (isset($data->data) AND isset($data->data['releases']) AND is_array($data->data['releases'])) {
+      return $data->data;
+    }
+  }
+  
+  return FALSE;
+}
+
+// ======================================
+// Batch Operations:
+// ======================================
+
+/**
+ * Helper function to download a project. This code is mostly copied and pasted from
+ * modules/update/update.manager.inc
+ * 
+ * There were no suitable functions that could be used besides 
+ * drupal_form_submit('update_manager_install_form', $form_state, $project['type']);
+ * and it wouldn't work because this is being run from a Batch function
+ * 
+ * @todo - Ideally, this should be in the update module as a standalone function, 
+ * to reduce coupling and duplication
+ */
+function project_browser_download_project($url) {
+  module_load_include('inc', 'update', 'update.manager');
+  // Download the file
+  $local_cache = update_manager_file_get($url);
+  if (!$local_cache) {
+    return array(
+      'success' => FALSE,
+      'message' => t('Unable to retrieve Drupal project from %url.', array('%url' => $url)),
+      );
+  }
+  
+  // Try to extract it
+  $directory = _update_manager_extract_directory();
+  try {
+    $archive = update_manager_archive_extract($local_cache, $directory);
+  }
+  catch (Exception $e) {
+    return array(
+      'success' => FALSE,
+      'message' => $e->getMessage(),
+      );
+  }
+  $files = $archive->listContents();
+  if (!$files) {
+    return array(
+      'success' => FALSE,
+      'message' => t('Provided archive contains no files.'),
+      );
+  }
+  
+  $project = strtok($files[0], '/\\');
+
+  $archive_errors = update_manager_archive_verify($project, $local_cache, $directory);
+  if (!empty($archive_errors)) {
+    if (!empty($archive_errors)) {
+      foreach ($archive_errors as $error) {
+        drupal_set_message(check_plain($error), 'error');
+      }
+    }
+    return array(
+      'success' => FALSE,
+      'message' => array_shift($archive_errors),
+      );
+  }
+	//require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/Updater/Updater.php';
+
+  // Make sure the Updater registry is loaded.
+  drupal_get_updaters();
+
+  $project_location = $directory . '/' . $project;
+  try {
+    $updater = Updater::factory($project_location);
+  }
+  catch (Exception $e) {
+    return array(
+      'success' => FALSE,
+      'message' => $e->getMessage(),
+      );
+  }
+
+  try {
+    $project_title = Updater::getProjectTitle($project_location);
+  }
+  catch (Exception $e) {
+    return array(
+      'success' => FALSE,
+      'message' => $e->getMessage(),
+      );
+  }
+
+  if ($updater->isInstalled()) {
+    return array(
+      'success' => FALSE,
+      'message' => t('%project is already installed.', array('%project' => $project_title)),
+      );
+  }
+
+  $project_real_location = drupal_realpath($project_location);
+  $updater_name = get_class($updater);
+  
+  if (fileowner($project_real_location) == fileowner(conf_path())) {
+    module_load_include('inc', 'update', 'update.authorize');
+    $filetransfer = new Local(DRUPAL_ROOT);
+    
+    // Initialize some variables in the Batch API $context array.
+    $updater = new $updater_name($project_real_location);
+  
+    try {
+      if ($updater->isInstalled()) {
+        // This is an update.
+        $tasks = $updater->update($filetransfer);
+      }
+      else {
+        $tasks = $updater->install($filetransfer);
+      }
+    }
+    catch (UpdaterException $e) {
+      return array(
+        'success' => FALSE,
+        'message' => t('Error installing / updating. Error: @error', array('@error' => $e->getMessage())),
+        );
+    }
+  }
+  else {
+    return array(
+      'success' => FALSE,
+      'message' => t('Permissions are not set up properly.'),
+      );
+  }
+  
+  return array(
+    'success' => TRUE,
+    );
+}
+
+/**
+ * Installs a single release of a project during batch, for example
+ */
+function _project_browser_batch_install_release($release_name, $project, &$context) {
+  module_load_include('inc', 'project_browser', 'project_browser.pages');
+  $release = project_browser_get_release($release_name, $project);
+  
+  $result = project_browser_download_project($release['download_link']);
+  
+  if ($result['success']) {
+    $context['results']['successes'][] = t('Successfully installed %project.', array('%project' => $project['title']));
+    $context['message'] = t('Installed %project...', array('%project' => $project['title']));
+    
+    // Add this to the session variable and remove it from the install_queue variable
+    $_SESSION['project_browser_installed_projects'][$project['name']] = $project;
+    unset($_SESSION['project_browser_install_list'][$project['name']]);
+  }
+  else {
+    watchdog('project_browser', 'There was an error while installing %project. 
+      !message',
+      array('%project' => $project['title'], '!message' => $result['message']), WATCHDOG_ERROR);
+    $context['results']['failures'][] = t('Error installing %project. Errors have been logged.', 
+      array('%project' => $project['title']));
+    $context['message'] = t('Error installing %project. !message', 
+      array('%project' => $project['title'], '!message' => $result['message']));
+  }
+}
+
+/**
+ * Shows a message and finish up the batch
+ */
+function _project_browser_batch_install_releases_finished($success, $results, $operations) {
+  drupal_get_messages();
+  
+  // Restore the maintenance mode to what it was at the start
+  variable_set('maintenance_mode', $_SESSION['maintenance_mode']);
+  unset($_SESSION['maintenance_mode']);
+  
+  unset($_SESSION['project_browser_install_releases_list']);
+  if ($success) {
+    if (!empty($results)) {
+      if (!empty($results['failures'])) {
+        drupal_set_message(format_plural(count($results['failures']), 'Failed to install one project.', 'Failed to install @count projects.'), 'error');
+      }
+    }
+  }
+  else {
+    drupal_set_message(t('Error installing projects.'), 'error');
+    drupal_goto('admin/modules/project-browser/install/select_versions');
+  }
+	
+	$projects = project_browser_get_installed_projects();
+	$missing = project_browser_get_missing_dependencies($projects);
+	// If there are missing dependencies, go to install dependencies
+	if (count($missing) > 0) {
+		drupal_goto('admin/modules/project-browser/install/install_dependencies');
+	}
+	else {
+  	drupal_goto('admin/modules/project-browser/install/enable');
+  }
+}
+
+/**
+ * Gets the dependencies for installed projects
+ */
+function project_browser_get_missing_dependencies($projects) {
+	$modules = system_rebuild_module_data();
+	
+	$missing = array();
+  
+  foreach ($projects as $project) {
+    if ($project['type'] == 'module') {
+      $dependency_check = TRUE;
+      $dependencies = array();
+      if (isset($modules[$project['name']])) {
+        foreach ($modules[$project['name']]->info['dependencies'] as $dependency) {
+          if (!isset($modules[$dependency])) {
+            $dependencies[] = $dependency;
+          }
+        }
+        if (count($dependencies) > 0) {
+          $missing[$project['name']] = $dependencies;
+        }
+      }
+      else {
+        drupal_set_message(t('There was an error getting information for @module',
+          array('@module' => $project['name'])), 'error');
+      }
+    }
+  }
+	
+	return $missing;
+}
diff --git a/core/modules/project_browser/project_browser.info b/core/modules/project_browser/project_browser.info
new file mode 100644
index 0000000..c6510a8
--- /dev/null
+++ b/core/modules/project_browser/project_browser.info
@@ -0,0 +1,7 @@
+name = Project Browser
+description = A Project Browser that allows users to browse for and install modules and themes from their Drupal site admin area
+dependencies[] = update
+package = Core
+version = VERSION
+core = 8.x
+files[] = project_browser.test
\ No newline at end of file
diff --git a/core/modules/project_browser/project_browser.module b/core/modules/project_browser/project_browser.module
new file mode 100644
index 0000000..3744bfb
--- /dev/null
+++ b/core/modules/project_browser/project_browser.module
@@ -0,0 +1,467 @@
+<?php
+/**
+ * @file
+ * Project Browser module.
+ * Authored by Leighton Whiting for Google Summer of Code 2011
+ * 
+ * This module provides a new UI for admins to easily browse modules and themes from their
+ * admin pages, and install them.
+ * 
+ * TODO - Add a context to pages showing the filters currently used
+ * TODO - Create a test for the get releases HTML page
+ */
+
+/**
+ * Display help and module information
+ */
+function project_browser_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case "admin/help#project_browser":
+      $output .= '<p>' . t("Provides a UI for users to browse for and install new modules and themes from
+      within their Drupal admin interface.") . '</p>';
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implements of hook_perm().
+ */
+function project_browser_permission() {
+  return array(
+    'use project browser' => array(
+      'title' => t('Use Project Browser'),
+      'description' => t('This allows the user to browse for and install new modules and themes using Project Browser.'),
+      'restrict access' => TRUE,
+    )
+  );
+}
+
+/**
+ * Implements of hook_menu().
+ */
+function project_browser_menu() {
+  $items = array();
+  $items['admin/config/development/project_browser'] = array(
+    'title' => 'Project Browser settings',
+    'description' => 'Add new repositories and set other settings for Project Browser.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_browser_admin'),
+    'access arguments' => array('access administration pages'),
+    'file' => 'project_browser.admin.inc',
+  );
+  
+  $items['admin/modules/project-browser'] = array(
+    'title' => 'Project Browser',
+    'description' => 'Browse and search for new modules',
+    'page callback' => 'project_browser_page',
+    'page arguments' => array('module'),
+    'access arguments' => array('use project browser'),
+    'file' => 'project_browser.pages.inc',
+  );
+  
+  $items['admin/modules/project-browser/modules'] = array(
+    'title' => 'Modules',
+    'description' => 'Browse and search for new modules',
+    'page callback' => 'project_browser_page',
+    'page arguments' => array('module'),
+    'access arguments' => array('use project browser'),
+    'file' => 'project_browser.pages.inc',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  
+  $items['admin/modules/project-browser/themes'] = array(
+    'title' => 'Themes',
+    'description' => 'Browse and search for new themes',
+    'page callback' => 'project_browser_page',
+    'page arguments' => array('theme'),
+    'access arguments' => array('use project browser'),
+    'file' => 'project_browser.pages.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+  
+  $items['admin/modules/project-browser/install/%'] = array(
+    'title' => 'Install',
+    'page callback' => 'project_browser_installation_page',
+    'page arguments' => array(4),
+    'access arguments' => array('use project browser'),
+    'type' => MENU_NORMAL_ITEM,
+    'file' => 'project_browser.pages.inc',
+  );
+  
+  $items['project-browser/%/install-queue/%/%'] = array(
+    'page callback' => 'project_browser_install_queue_callback',
+    'page arguments' => array(1, 3, 4),
+    'access arguments' => array('use project browser'),
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
+/**
+ * Page callback that allows for adding to and removing from the install queue
+ */
+function project_browser_install_queue_callback($method, $op, $project_name) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+	
+	switch ($op) {
+    case 'add':
+			$projects = project_browser_get_listed_projects();
+  
+		  if (isset($projects[$project_name])) {
+		    $project = $projects[$project_name];
+		    project_browser_install_queue_add($project);
+		  }
+		  else {
+		    drupal_set_message(t('Error: The project was not found.'), 'error');
+		  }
+      break;
+			
+    case 'remove':			
+      project_browser_install_queue_remove($project_name);
+      break;
+  }
+	
+  switch ($method) {
+    case 'nojs':
+      // Redirect to the page it came from
+      $redirect = (isset($_GET['destination'])) ? $_GET['destination'] : 'admin/modules/project-browser';
+      
+      drupal_goto($redirect);
+      break;
+    case 'ajax':
+      $commands = array();
+      
+      // Refresh the install queue
+      $commands[] = ajax_command_replace('#project-browser-install-queue', project_browser_get_install_list());
+      // Refresh the add to queue link
+      $commands[] = ajax_command_replace('#add-to-queue-link-' . $project_name, project_browser_add_remove_queue_link($project_name));
+
+      return array('#type' => 'ajax', '#commands' => $commands);
+      break;
+  }
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ * 
+ * This is used to put the 'Project Browser' action on the 'Modules' page
+ */
+function project_browser_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+  switch ($root_path) {
+    case 'admin/modules':
+      // Unset the install theme page
+      foreach ($data['actions']['output'] as $num => $item) {
+        if ($item['#link']['path'] == 'admin/modules/install') {
+          unset($data['actions']['output'][$num]);
+        }
+      }
+      $item = menu_get_item('admin/modules/project-browser/modules');
+      if ($item['access']) {
+        $item['title'] = t('Install new modules');
+        $data['actions']['output'][] = array(
+          '#theme' => 'menu_local_action',
+          '#link' => $item,
+        );
+      }
+      break;
+    case 'admin/modules/project-browser':
+    case 'admin/modules/project-browser/modules':
+    case 'admin/modules/project-browser/themes':
+      $item = menu_get_item('admin/modules/install');
+      if ($item['access']) {
+        $item['title'] = t('Install manually');
+        $data['actions']['output'][] = array(
+          '#theme' => 'menu_local_action',
+          '#link' => $item,
+        );
+      }
+      break;
+    case 'admin/appearance':
+      // Unset the install theme page
+      foreach ($data['actions']['output'] as $num => $item) {
+        if ($item['#link']['path'] == 'admin/appearance/install') {
+          unset($data['actions']['output'][$num]);
+        }
+      }
+      $item = menu_get_item('admin/modules/project-browser/themes');
+      if ($item['access']) {
+        $item['title'] = t('Install new themes');
+        $data['actions']['output'][] = array(
+          '#theme' => 'menu_local_action',
+          '#link' => $item,
+        );
+      }
+      break;
+  }
+}
+
+// ======================================
+// Theme:
+// ======================================
+
+/**
+ * Implements hook_theme() 
+ */
+function project_browser_theme($existing, $type, $theme, $path) {
+  return array(
+    // Template for installation page
+    'project_browser_install' => array(
+      'variables' => array('current_task' => NULL, 'main_content' => NULL),
+      'path' => $path . '/theme',
+      'template' => 'project-browser-install',
+    ),
+    // Template for list of projects
+    'project_browser_list' => array(
+      'variables' => array('projects_list' => NULL, 'type' => NULL),
+      'path' => $path . '/theme',
+      'template' => 'project-browser-list',
+    ),
+    // Template for list of projects
+    'project_browser_block' => array(
+      'render element' => 'element',
+      'path' => $path . '/theme',
+      'template' => 'project-browser-block',
+    ),
+    // Template for single project
+    'project_browser_project' => array(
+      'variables' => array('project' => NULL, 'first' => NULL),
+      'path' => $path . '/theme',
+      'template' => 'project-browser-project',
+    ),
+    // Template for install queue item
+    'project_browser_install_queue' => array(
+      'variables' => array('projects' => NULL),
+      'path' => $path . '/theme',
+      'template' => 'project-browser-install-queue',
+    ),
+  );
+}
+
+/**
+ * Add some variables for the projects install theme
+ * 
+ * @param $variables
+ *   An associative array containing:
+ *     - current_task : the current task
+ */
+function project_browser_preprocess_project_browser_install(&$variables) {
+  module_load_include('inc', 'project_browser', 'project_browser.pages');
+  // Add the themed list
+  $variables['task_list'] = project_browser_installation_task_list($variables['current_task']);
+}
+
+/**
+ * Add some variables for the projects install queue theme
+ */
+function project_browser_preprocess_project_browser_install_queue(&$variables) {
+  $build = array();
+  if (empty($variables['projects'])) {
+    $build['empty_text'] = array(
+      '#markup' => t('Install queue is empty.'),
+    );
+  }
+  else {
+    foreach ($variables['projects'] as $project) {
+      $build['queued-item-' . $project['name']] = array(
+        '#prefix' => "<div class='project-browser-install-queue-item'>",
+        '#markup' => project_browser_add_remove_queue_link($project['name'], $project['title'], 'remove-queue-link'),
+        '#suffix' => "</div>",
+      );
+    }
+    $build['install-link'] = drupal_get_form('project_browser_install_button_form');
+  }
+  
+  // Add the install button
+  $variables['queue_html'] = drupal_render($build);
+}
+
+/**
+ * Add some variables for the project browser block theme
+ * 
+ * @param $variables
+ */
+function project_browser_preprocess_project_browser_block(&$variables) {
+  // Add the title and content variables
+  $variables['title'] = $variables['element']['#title'];
+  $variables['content'] = $variables['element']['#content'];
+}
+
+/**
+ * Add some variables for the projects list theme
+ * 
+ * @param $variables
+ *   An associative array containing:
+ *     - projects_list : array of all projects
+ */
+function project_browser_preprocess_project_browser_list(&$variables) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  drupal_add_css(drupal_get_path('module', 'project_browser') . '/css/project_browser.css');
+  
+  if (is_array($variables['projects_list']) AND !empty($variables['projects_list'])) {
+    $content = '';
+    $first = TRUE;
+    // Theme each individual project and add to the list
+    foreach ($variables['projects_list'] as $project) {
+      $content .= theme('project_browser_project', array('project' => $project, 'first' => $first));
+      $first = FALSE;
+    }
+  }
+  else {
+    $content = t('No results found.');
+  }
+  
+  switch ($variables['type']) {
+    case 'module':
+      $title = t('Modules');
+      break;
+    case 'theme':
+      $title = t('Themes');
+      break;
+    default:
+      $title = t('Projects');
+      break;
+  }
+  
+  $main_content['project_browser_main_block'] = array(
+    '#theme' => 'project_browser_block',
+    '#title' => $title, 
+    '#content' => $content,
+    '#contextual_links' => array(
+      'project_browser' => array('admin/config/development/project_browser', array()),
+    ),
+  );
+  $variables['main_content'] = render($main_content);
+  
+  // Add the pager
+  $variables['pager'] = theme('pager', array('tags' => NULL));
+  
+  // Add the filters
+  $filters_form = drupal_get_form('project_browser_filters_form', $variables['type']);
+  $filters['project_browser_filters_block'] = array(
+    '#theme' => 'project_browser_block',
+    '#title' => t('Filters'), 
+    '#content' => drupal_render($filters_form),
+  );
+  $variables['filters'] = render($filters);
+  
+  // Add the install list
+  $install_list['project_browser_filters_block'] = array(
+    '#theme' => 'project_browser_block',
+    '#title' => t('Install queue'), 
+    '#content' => project_browser_get_install_list(),
+  );
+  $variables['install_list'] = render($install_list);
+}
+
+/**
+ * Add some variables for the project theme
+ * 
+ * @param $variables
+ *   An associative array containing:
+ *     - project : associative array of project variables
+ */
+function project_browser_preprocess_project_browser_project(&$variables) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  $project = $variables['project'];
+  
+  $variables['title'] = l($project['title'], check_url($project['project url']), 
+  	array('attributes' => array('target' => '_blank'), 'html' => TRUE));
+  $variables['author'] = t('Author: @author', array('@author' => $project['author']));
+  $variables['description'] = _filter_htmlcorrector(filter_xss($project['description']));
+  $variables['image'] = $project['image'];
+  $variables['last_updated'] = ($project['last updated']) ? t('Last Updated: @date', array('@date' => format_date($project['last updated'], 'long'))) : '';
+  
+  $extras = array();
+  
+  if ($project['maintenance status']) {
+    $extras[] = check_plain($project['maintenance status']);
+  }
+  if ($project['development status']) {
+    // We are not showing this because it isn't a good indicator right now
+    // $extras[] = check_plain($project['development status']);
+  }
+  if ($project['usage'] AND is_numeric($project['usage'])) {
+    $extras[] = format_plural($project['usage'], '1 Install', '@count Installs');
+  }
+  if ($project['rating']) {
+    $extras[] = check_plain($project['rating']);
+  }
+  
+  $variables['extras'] = implode(' | ', $extras);
+  
+  // Check if the project is installed
+  if (_project_browser_is_project_enabled($project['type'], $project['name'])) {
+    $variables['status'] = '<div class="install-disabled">Already installed</div>'; 
+    $variables['install'] = '';
+  }
+  elseif (drupal_get_filename($project['type'], $project['name'])) {
+    $variables['status'] = '<div class="install-disabled">Already downloaded</div>';
+    $variables['install'] = '';
+  }
+  else {
+    $variables['status'] = '';
+    $variables['install'] = project_browser_add_remove_queue_link($project['name']);
+  }
+}
+
+/**
+ * Builds the add/remove project to install queue link
+ */
+function project_browser_add_remove_queue_link($project_name, $title = NULL, $id_prefix = 'add-to-queue-link') {
+  $queued_projects = project_browser_get_queued_projects();
+  if (!$title) {
+    $title = isset($queued_projects[$project_name]) ? t('Remove from Install queue') : t('Add to Install queue');
+  }
+  $op = isset($queued_projects[$project_name]) ? 'remove' : 'add';
+  
+  $build['ajax_link'] = array(
+    '#type' => 'link',
+    '#title' => $title,
+    '#href' => 'project-browser/nojs/install-queue/' . $op . '/'. $project_name,
+    '#options' => array(
+      'query' => drupal_get_destination(),
+    ),
+    '#id' => $id_prefix . '-' . $project_name,
+    '#ajax' => array(
+      'effect' => 'fade',
+      'speed' => 1000,
+      'progress' => array(
+        'type' => 'throbber',
+        'message' => '',
+      ),
+    ),
+  );
+  
+  return drupal_render($build);
+}
+
+/**
+ * Implements hook_library().
+ * 
+ * This is used for the categories multiselect widget library
+ */
+function project_browser_library() {
+  // Library One.
+  $libraries['multiselect'] = array(
+    'title' => 'jQuery MultiSelect', 
+    'website' => 'http://example.com/library-1', 
+    'version' => '1.10a', 
+    'js' => array(
+      drupal_get_path('module', 'project_browser') . '/js/jquery.multiselect.min.js' => array(),
+    ), 
+    'css' => array(
+      drupal_get_path('module', 'project_browser') . '/css/jquery.multiselect.css' => array(
+        'type' => 'file', 
+        'media' => 'screen',
+      ),
+    ),
+    'dependencies' => array(
+      array('system', 'ui.widget'),
+      array('system', 'ui.dialog'),
+      array('system', 'ui.position'),
+    ),
+  );
+  return $libraries;
+}
diff --git a/core/modules/project_browser/project_browser.pages.inc b/core/modules/project_browser/project_browser.pages.inc
new file mode 100644
index 0000000..d042bf1
--- /dev/null
+++ b/core/modules/project_browser/project_browser.pages.inc
@@ -0,0 +1,521 @@
+<?php 
+/**
+ * @file
+ * 
+ * This include contains most of the page callbacks for the project browser
+ * 
+ */
+
+/**
+ * Page to display the projects.
+ * 
+ * @param $type
+ *   Type of project to view, ie 'module' or 'theme'
+ * 
+ * @return 
+ *   Themed list of the projects
+ */
+function project_browser_page($type) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  drupal_add_library('project_browser', 'multiselect');
+  drupal_add_js(drupal_get_path('module', 'project_browser') . '/js/project_browser_more_link.js');
+  drupal_add_js(drupal_get_path('module', 'project_browser') . '/js/project_browser_multiselect.js');
+  
+  // Set any filters in the session that are needed
+  if (isset($_GET['repository'])) {
+    $_SESSION['project_browser_sever_filter'] = $_GET['repository'];
+  }
+  if (isset($_GET['order_by'])) {
+    $_SESSION['project_browser_order_by_filter_' . $type] = $_GET['order_by'];
+  }
+  if (isset($_GET['sort'])) {
+    $_SESSION['project_browser_sort_filter_' . $type] = $_GET['sort'];
+  }
+  
+  // Build the filters
+  $drupal_version = explode('.', DRUPAL_CORE_COMPATIBILITY);
+  $filters = array(
+    'version' => $drupal_version[0],
+    'type' => $type,
+  );
+  
+  // Add filters
+  if (isset($_SESSION['project_browser_category_filter_' . $type])) {
+    $categories = array_filter($_SESSION['project_browser_category_filter_' . $type]);
+    if (!empty($categories)) {
+      $filters['categories'] = project_browser_prepare_categories($categories, $type);
+    }
+  }
+  if (isset($_SESSION['project_browser_text_filter_' . $type])) {
+    $filters['text'] = $_SESSION['project_browser_text_filter_' . $type];
+  }
+  if (isset($_SESSION['project_browser_order_by_filter_' . $type])) {
+    $filters['order_by'] = $_SESSION['project_browser_order_by_filter_' . $type];
+  }
+  if (isset($_SESSION['project_browser_sort_filter_' . $type])) {
+    $filters['sort'] = $_SESSION['project_browser_sort_filter_' . $type];
+  }
+  if (isset($_SESSION['project_browser_server_filter'])) {
+    $filters['server'] = $_SESSION['project_browser_server_filter'];
+  }
+  else {
+    $filters['server'] = 0;
+  }
+  $filters['requested'] = 10;
+  $filters['page'] = isset($_GET['page']) ? $_GET['page'] : 0;
+  
+  // Get the projects to display here based on the filters
+  $results = project_browser_fetch_results($filters);
+  
+  // Save the listed projects in the session so it can be used
+  $_SESSION['project_browser_listed_projects'] = $results['projects'];
+  
+  $test = project_browser_get_listed_projects();
+  
+  $list = array();
+  foreach ($results['projects'] as $project) {
+    $list[] = $project;
+  }
+  
+  // Add the pager
+  $total = $results['total'];
+  $num_per_page = 10;
+  $page = pager_default_initialize($total, $num_per_page);
+  $offset = $num_per_page * $page;
+  $start = ($total) ? $offset + 1 : 0;
+  $finish = $offset + $num_per_page;
+  if ($finish > $total) {
+    $finish = $total;
+  }
+  
+  $sort_options = project_browser_get_sort_options();
+  $current_order_by = isset($_SESSION['project_browser_order_by_filter_' . $type]) ? $_SESSION['project_browser_order_by_filter_' . $type] : 'score';
+  $current_sort = isset($_SESSION['project_browser_sort_filter_' . $type]) ? $_SESSION['project_browser_sort_filter_' . $type] : 'desc';;
+  
+  $build = array();
+  $build['content'] = array(
+    'project_browser_header' => array(
+      '#markup' => t('Showing @start to @finish of @total.', array(
+        '@start' => $start, '@finish' => $finish, '@total' => $total)),
+      '#weight' => 0,
+    ),
+    'project_browser_sort_header' => array(
+      '#type' => 'item',
+      '#weight' => 2,
+      '#markup' => project_browser_get_sort_widget($sort_options, $current_order_by, $current_sort),
+    ),
+    'project_browser_list' => array(
+      '#markup' => theme('project_browser_list', array('projects_list' => $list, 'type' => $type)),
+      '#weight' => 3,
+    ),
+    'pager' => array(
+      '#theme' => 'pager',
+      '#weight' => 99,
+    ),
+  );
+  
+  $servers = project_browser_get_servers();
+  
+  if (count($servers) > 1) {
+    $build['content']['project_browser_server_header'] = array(
+      '#type' => 'item',
+      '#weight' => 1,
+      '#markup' => project_browser_get_server_widget($servers, $filters['server']),
+    );
+  }
+  
+  return $build;
+}
+
+// ======================================
+// Installation Page:
+// ======================================
+
+/**
+ * Page to install projects
+ * 
+ * @param $op
+ *    Operation to preform.
+ */
+function project_browser_installation_page($op) {
+  drupal_add_css(drupal_get_path('module', 'project_browser') . '/css/project_browser.css', array('preprocess' => FALSE));
+  
+  switch ($op) {
+    case 'select_versions':
+      drupal_set_title(t("Select versions"));
+      $content = project_browser_installation_select_versions_page();
+      break;
+		case 'install_dependencies':
+      drupal_set_title(t("Install Dependencies"));
+      $content = project_browser_installation_install_dependencies_page();
+      break;
+    case 'enable':
+      drupal_set_title(t("Enable modules"));
+      $content = project_browser_installation_enable_page();
+      break;
+  }
+  return theme('project_browser_install', array('current_task' => $op, 'main_content' => drupal_render($content)));
+}
+
+/**
+ * Task page for the Select versions installation task
+ * 
+ * Shows a form where the user can select which versions to install for each 
+ * project
+ */
+function project_browser_installation_select_versions_page() {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  // Show a form that lets the user select which version of the projects to install
+  $queued_projects = project_browser_get_queued_projects();
+	unset($_SESSION['project_browser_installed_projects']);
+  
+  return drupal_get_form('project_browser_installation_select_versions_form', $queued_projects);
+}
+
+/**
+ * Form builder for the select versions form
+ * 
+ * @param $projects
+ *   An array of projects to get the releases for
+ */
+function project_browser_installation_select_versions_form($form, &$form_state, $projects) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  drupal_add_js(drupal_get_path('module', 'project_browser') . '/js/select_releases.js');
+  
+  $form = array();
+  
+  // First unset any old data
+  unset($_SESSION['project_browser_install_releases_list']);
+  
+  $form['#tree'] = TRUE;
+  
+  $form['releases-header'] = array(
+    '#type' => 'item',
+    '#markup' => t("You're about to install:"),
+  );
+  
+  $form['releases'] = array();
+  
+  foreach ($projects as $project) {
+    // Get the available releases for this project
+    if (!$release_data = project_browser_get_project_release_data($project)) {
+    	drupal_set_message(t('Could not fetch releases for project %project.',
+	      array('%project' => $project['title'])), 'warning');
+    	watchdog('project_browser', 'Could not fetch releases for project %project.',
+	      array('%project' => $project['title']), WATCHDOG_ERROR);
+			project_browser_install_queue_remove($project['name']);
+			continue;
+		}
+    
+    // We use the update module to calculate the recommended version
+    $project_data = array(
+      'existing_major' => 0,
+      'existing_version' => 0,
+      'install_type' => '',
+    );
+    module_load_include('inc', 'update', 'update.compare');
+    update_calculate_project_update_status($project_data, $release_data);
+
+    $releases_list = array();
+    
+    foreach ($release_data['releases'] as $version => $release) {
+      $release_title = t("@project @version - @date", array(
+        '@project' => $project['title'],
+        '@version' => $release['version'],
+        '@date' => format_date($release['date'], 'custom', 'M j, Y'),
+        ));
+      if (isset($release['terms']['Release type']) AND !empty($release['terms']['Release type'])) {
+        $release_title .= " (" . implode(', ', $release['terms']['Release type']) . ")";
+      }
+      if (isset($release['release_link'])) {
+        $releases_list[$version] = l($release_title, $release['release_link']);
+      }
+      else {
+        $releases_list[$version] = $release_title;
+      }
+    }
+    
+    $form['releases'][$project['name']]['project'] = array(
+      '#type' => 'value',
+      '#value' => $project,
+    );
+    
+    $form['releases'][$project['name']]['release_name'] = array(
+      '#type' => 'radios',
+      '#title' => t('Select release for @project', array('@project' => $project['title'])),
+      '#options' => $releases_list,
+      '#default_value' => key($releases_list),
+      '#prefix' => '<div class="project-browser-releases-wrapper project-browser-release-' . $project['name'] . '">',
+      '#suffix' => '</div>',
+      '#attributes' => array(
+        'class' => array('project-browser-releases-radios'),
+      ),
+      '#required' => TRUE,
+    );
+    $form['releases'][$project['name']]['selected_text'] = array(
+      '#type' => 'item',
+      '#prefix' => '<div class="project-browser-selected-release project-browser-selected-release-' . $project['name'] . '">',
+      '#suffix' => '</div>',
+      '#markup' => reset($releases_list),
+    );
+    if (isset($project_data['recommended'])) {
+      // If there is a recommended release set, then only show it and show the jQuery link
+      $recommended_releases = array();
+      $recommended_releases[$project_data['recommended']] = $releases_list[$project_data['recommended']];
+      $form['releases'][$project['name']]['release_name']['#default_value'] = $project_data['recommended'];
+      $form['releases'][$project['name']]['selected_text']['#markup'] = $releases_list[$project_data['recommended']];
+    }
+    if (count($releases_list) > 1) {
+      $form['releases'][$project['name']]['selected_text']['#markup'] .= 
+        " (<span class='project-browser-show-releases-link' rel='" . $project['name'] . "'>" . t('change release') . "</span>)";
+    }
+  }
+
+	// If there is nothing to install, go to the enable page
+	if (empty($form['releases'])) {
+		drupal_set_message(t('No releases data found for any of the selected projects.'), 'warning');
+		drupal_goto('admin/modules/project-browser/install/enable');
+	}
+	
+  $form['backup_warning'] = array(
+    '#type' => 'markup',
+    '#markup' => t('Back up your database and site before you continue. !link.',
+      array('!link' => l(t('Learn how'), 'http://drupal.org/node/22281'))),
+  );
+  $form['maintenance_mode'] = array(
+    '#type' => 'checkbox', 
+    '#title' => t('Perform updates with site in maintenance mode (strongly recommended)'), 
+    '#default_value' => TRUE, 
+  );
+  
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Install'),
+  );
+  
+  return $form;
+}
+
+/**
+ * Submit handler for the select versions form
+ */
+function project_browser_installation_select_versions_form_submit($form, &$form_state) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  // Store maintenance_mode setting so we can restore it when done.
+  $_SESSION['maintenance_mode'] = variable_get('maintenance_mode', FALSE);
+  if ($form_state['values']['maintenance_mode'] == TRUE) {
+    variable_set('maintenance_mode', TRUE);
+  }
+  
+  foreach ($form_state['values']['releases'] as $item) {
+    // Load the selected release
+    if ($release = project_browser_get_release($item['release_name'], $item['project'])) {
+      // Add the release to a session variable
+      $_SESSION['project_browser_install_releases_list'][$item['release_name']] = $item['project'];
+    }
+  }
+  
+  // Install the projects with batch
+  module_load_include('inc', 'update', 'update.manager');
+  
+  $queued_releases = project_browser_get_queued_releases();
+  
+  $operations = array();
+  foreach ($queued_releases as $release_name => $project) {
+    $operations[] = array('_project_browser_batch_install_release', array($release_name, $project));
+  }
+  $batch = array(
+    'operations' => $operations,
+    'finished' => '_project_browser_batch_install_releases_finished',
+    'title' => t('Installing projects'),
+    'init_message' => t('Installing modules...'),
+    'progress_message' => t('Installed @current out of @total.'),
+    'error_message' => t('Installation has encountered an error.'),
+    'file' => drupal_get_path('module', 'project_browser') . '/project_browser.inc',
+  );
+  batch_set($batch);
+}
+
+/**
+ * Task page for the Install Dependencies installation task
+ * 
+ * Show a form which lets the user select which version of dependencies to install
+ */
+function project_browser_installation_install_dependencies_page() {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  $projects = project_browser_get_installed_projects();
+	$missing = project_browser_get_missing_dependencies($projects);
+  
+	if (count($missing) > 0) {
+		$missing_projects = array();
+	  // Add the project data in the array as best we can
+	  foreach ($missing as $project_shortname => $dependencies) {
+	  	foreach ($dependencies as $shortname) {
+	  		$missing_projects[$shortname] = array(
+					'name' => $shortname,
+					'type' => 'module', // FIXME
+					'title' => $shortname,
+				);
+	  	}
+	  }
+	  
+	  return drupal_get_form('project_browser_installation_select_versions_form', $missing_projects);
+  }
+	else {
+		drupal_goto('admin/modules/project-browser/install/enable');
+	}
+}
+
+/**
+ * Task page for the Enable projects installation task
+ * 
+ * Show a form which lets the user enable the newly installed projects
+ */
+function project_browser_installation_enable_page() {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  $installed_projects = project_browser_get_installed_projects();
+	
+	if (count($installed_projects) > 0) {
+  	return drupal_get_form('project_browser_installation_enable_form', $installed_projects);
+	}
+	else {
+		drupal_goto('admin/modules/project-browser');
+	}
+}
+
+/**
+ * Form builder for the select versions form
+ * 
+ * @param $projects
+ *   An array of projects to get the releases for
+ */
+function project_browser_installation_enable_form($form, &$form_state, $projects) {
+  $modules = system_rebuild_module_data();
+  $form['instructions'] = array(
+    '#type' => 'item',
+    '#markup' => t('The projects you selected have been successfully installed. 
+      If you installed any new modules, you may enable them using the form below 
+      or on the main !link page.', array('!link' => l(t('Modules'), 'admin/modules'))),
+  );
+  
+  $options = array();
+  $missing = array();
+  
+  foreach ($projects as $project) {
+    if ($project['type'] == 'module') {
+      $dependency_check = TRUE;
+      $dependencies = array();
+      if (isset($modules[$project['name']])) {
+        foreach ($modules[$project['name']]->info['dependencies'] as $dependency) {
+          if (isset($modules[$dependency])) {
+            $dependencies[] = $modules[$dependency]->info['name'] . ' (' . t('Installed') . ')';
+          }
+          else {
+            $dependency_check = FALSE;
+            $dependencies[] = $dependency . ' (' . t('Missing') . ')';
+          }
+        }
+        if ($dependency_check) {
+          $options[$project['name']] = array(
+            array('data' => $modules[$project['name']]->info['name']),
+            array('data' => $modules[$project['name']]->info['version']),
+            array('data' => implode(', ', $dependencies)),
+          );
+        }
+        else {
+          $missing[$project['name']] = array(
+            array('data' => $modules[$project['name']]->info['name']),
+            array('data' => $modules[$project['name']]->info['version']),
+            array('data' => implode(', ', $dependencies)),
+          );
+        }
+      }
+      else {
+        drupal_set_message(t('There was an error getting information for @module',
+          array('@module' => $project['name'])), 'error');
+      }
+    }
+  }
+
+  $headers = array(
+    array('data' => t('Title')),
+    array('data' => t('Version')),
+    array('data' => t('Dependencies')),
+  );
+  
+  if (!empty($options)) {
+    $form['modules'] = array(
+      '#type' => 'tableselect', 
+      '#title' => t('Enable modules'),
+      '#description' => t('Select which modules you would like to enable.'),
+      '#header' => $headers, 
+      '#options' => $options, 
+      '#empty' => t('No new modules installed.'), 
+      '#multiple' => TRUE,
+      '#js_select' => TRUE,
+      '#weight' => 1,
+    );
+    
+    $form['submit'] = array(
+      '#type' => 'submit',
+      '#submit' => array('project_browser_installation_enable_form_submit'),
+      '#value' => t('Enable modules'),
+      '#weight' => 99,
+    );
+  }
+  
+  if (!empty($missing)) {
+    $form['missing'] = array(
+      '#type' => 'item', 
+      '#title' => t('Missing Dependencies'),
+      '#description' => t('These modules are missing one or more dependencies, 
+        and so cannot be enabled.'),
+      '#markup' => theme('table', array('header' => $headers, 'rows' => $missing)), 
+      '#weight' => 2,
+    );
+  }
+  
+  return $form;
+}
+
+/**
+ * Form submit handler
+ */
+function project_browser_installation_enable_form_submit($form, &$form_state) {
+  $enable_queue = array_filter($form_state['values']['modules']);
+  // Enable these all at once so that dependencies are handled properly
+  module_enable($enable_queue);
+  
+	/*
+  // Enable the modules one by one
+  foreach ($enable_queue as $project_name) {
+    if (!module_enable(array($project_name))) {
+      drupal_set_message(t('Error installing a module: @module', array('@module' => $project_name)), 'error');
+    }
+  }
+	*/
+	drupal_flush_all_caches();
+  
+  drupal_goto('admin/modules');
+}
+
+/**
+ * Get a task list to the sidebar area when installing projects
+ * 
+ * This will need to be called from every page of the install process
+ * 
+ * @param $active
+ *   (Optional) Set the active task by key
+ */
+function project_browser_installation_task_list($active = NULL) {
+  // Default list of tasks.
+  $tasks = array(
+    'select_versions' => t('Select versions'),
+    'install_dependencies' => t('Install Dependencies'),
+    'enable' => t('Enable projects'),
+  );
+  
+  require_once DRUPAL_ROOT . '/core/includes/theme.maintenance.inc';
+  
+  return theme_task_list(array('items' => $tasks, 'active' => $active));
+}
diff --git a/core/modules/project_browser/project_browser.test b/core/modules/project_browser/project_browser.test
new file mode 100644
index 0000000..71d1b13
--- /dev/null
+++ b/core/modules/project_browser/project_browser.test
@@ -0,0 +1,75 @@
+<?php
+/**
+* Administration tests for Project Browser.
+*/
+class ProjectBrowserAdministrationTestCase extends DrupalWebTestCase {
+  protected $privileged_user;
+  
+  /**
+   * Implements getInfo().
+   */
+  public static function getInfo() {
+    return array (
+      'name' => t('Project Browser Install Project Test'),
+      'description' => t('Attempts to install a project.'),
+      'group' => t('Project Browser'),
+    );
+  }
+  
+  public function setUp() {
+    parent::setUp('project_browser', 'project_browser_test');  // Enable any modules required for the test
+    
+    // Set the default server variable
+    $server_url = url('project_browser_test/query', array('absolute' => TRUE));
+    variable_set('project_browser_default_server', array(
+      $server_url => array(
+        'name' => 'Test Server',
+        'method' => 'json',
+      ),
+    ));
+    
+    // Create and log in our privileged user.
+    $this->privileged_user = $this->drupalCreateUser(array(
+      'use project browser',
+    ));
+    $this->drupalLogin($this->privileged_user);
+  }
+  
+  public function testProjectBrowserSearchViews() {
+    // Create node to edit.    
+    $edit = array();
+    $edit['search_text'] = 'views';
+    $this->drupalPost('admin/modules/project-browser/modules', $edit, t('Filter'));
+    $this->assertText('Showing 1 to');
+  }
+  
+  public function testProjectBrowserGetProjects() {
+    // Attempt to fetch the default projects   
+    $edit = array();
+    $edit['search_text'] = '';
+    $this->drupalPost('admin/modules/project-browser/modules', $edit, t('Filter'));
+    $this->assertText('Showing 1 to');
+  }
+
+  public function testProjectBrowserProjectEnabled() {
+    // Make sure project enabled detection works 
+    module_load_include('inc', 'project_browser', 'project_browser');
+    $this->assertTrue(_project_browser_is_project_enabled('module', 'project_browser'), t('Make sure project enabled detection works.'));
+  }
+
+  public function testProjectBrowserAddRemoveQueue() {
+    // Refresh the page
+    $this->drupalGet('admin/modules/project-browser/modules');
+    
+    // Simulate adding a project to the install queue
+    $this->drupalGet('project-browser/nojs/install-queue/add/views', array('query' => array('destination' => 'admin/modules/project-browser')));
+    $this->assertNoText('Install queue is empty.');
+    $this->assertNoText('Error: The project was not found.');
+    
+    // Simulate removing a project from the install queue
+    $this->drupalGet('project-browser/nojs/install-queue/remove/views', array('query' => array('destination' => 'admin/modules/project-browser')));
+    $this->assertText('Install queue is empty.');
+    $this->assertNoText('Error: The project was not found.');
+  }
+}
+?>
\ No newline at end of file
diff --git a/core/modules/project_browser/tests/project_browser_test.info b/core/modules/project_browser/tests/project_browser_test.info
new file mode 100644
index 0000000..6db51d2
--- /dev/null
+++ b/core/modules/project_browser/tests/project_browser_test.info
@@ -0,0 +1,12 @@
+name = "Project Browser module tests"
+description = "Support module for Project Browser related testing."
+package = Testing
+core = 7.x
+hidden = TRUE
+
+; Information added by drupal.org packaging script on 2012-01-20
+version = "7.x-1.x-dev"
+core = "7.x"
+project = "project_browser"
+datestamp = "1327063068"
+
diff --git a/core/modules/project_browser/tests/project_browser_test.module b/core/modules/project_browser/tests/project_browser_test.module
new file mode 100644
index 0000000..6f9126a
--- /dev/null
+++ b/core/modules/project_browser/tests/project_browser_test.module
@@ -0,0 +1,329 @@
+<?php
+
+/**
+ * Implements hook_menu().
+ */
+function project_browser_test_menu() {
+  $items['project_browser_test/query'] = array(
+    'title' => 'Test Query page',
+    'description' => "Tests the ability to fetch and display projects, and filter them appropriately.",
+    'page callback' => 'project_browser_test_query',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
+/**
+ * Page callback. Generates json based on the input filters
+ */
+function project_browser_test_query() {
+  if (!isset($_GET['method'])) {
+    print drupal_json_encode(t('You must specify a method.'));
+    exit();
+  }
+  switch ($_GET['method']) {
+    case 'categories':
+      if (!isset($_GET['type'])) {
+        print drupal_json_encode(t('You must specify a project type.'));
+        exit();
+      }
+      
+      $categories['project_browser_test'] = project_browser_test_get_categories($_GET['type']);
+      
+      print drupal_json_encode($categories);
+      exit();
+    
+    case 'query':
+      // Check that we have valid data
+      if (!isset($_GET['drupal_version'])) {
+        print drupal_json_encode(t('You must specify a drupal version.'));
+        exit();
+      }
+      if (!isset($_GET['type'])) {
+        print drupal_json_encode(t('You must specify a project type.'));
+        exit();
+      }
+      
+      // Get the filters
+      $filters = array(
+        'drupal_version' => $_GET['drupal_version'],
+        'type' => $_GET['type'],
+        'text' => (isset($_GET['text']) AND $_GET['text']) ? $_GET['text'] : '',
+        'sort_method' => isset($_GET['sort_method']) ? $_GET['sort_method'] : 'usage',
+        'sort_direction' => isset($_GET['sort_direction']) ? $_GET['sort_direction'] : 'desc',
+        'requested' => isset($_GET['requested']) ? (int) $_GET['requested'] : 12,
+        'page' => isset($_GET['page']) ? (int) $_GET['page'] : 0,
+      );
+      
+      if (isset($_GET['categories'])) {
+        $categories = unserialize($_GET['categories']);
+        if (is_array($categories) AND !empty($categories)) {
+          $filters['categories'] = $categories;
+        }
+      }
+      
+      // Pass them off to the project_browser_server_get_results() function
+      $results = project_browser_test_get_results($filters);
+      
+      print drupal_json_encode($results);
+      
+      exit();
+  }
+}
+
+/**
+ * Returns some static categories
+ */
+function project_browser_test_get_categories($type) {
+  $categories = array();
+  
+  switch ($type) {
+    case 'module':
+      $categories = array(
+        'admin' => "Administrative",
+        'search' => "Search",
+        'user_management' => "User Management",
+      );
+      break;
+      
+    case 'theme':
+      $categories = array(
+        'dark' => "Dark",
+        'light' => "Light",
+      );
+  }
+
+  return $categories;
+}
+
+/**
+ * Returns projects based on the filters
+ */
+function project_browser_test_get_results($filters) {
+  $projects = project_browser_test_projects();
+  
+  $results = array(
+    'total' => count($projects),
+    'projects' => array(),
+  );
+  
+  // Filter out projects based on type
+  if (isset($filters['type']) AND $type = $filters['type']) {
+    foreach ($projects as $name => $project) {
+      if ($type != $project['type']) {
+        unset($projects[$name]);
+      }
+    }
+  }
+  
+  // Filter out projects based on drupal version number
+  if (isset($filters['drupal_version']) AND $version = $filters['drupal_version']) {
+    foreach ($projects as $name => $project) {
+      if ($version != $project['drupal version']) {
+        unset($projects[$name]);
+      }
+    }
+  }
+  
+  // Filter out projects based on categories number
+  if (isset($filters['categories']) AND is_array($filters['categories']) AND !empty($filters['categories'])) {
+    $filtered = array();
+    foreach ($projects as $name => $project) {
+      foreach ($project['categories'] as $category) {
+        if (in_array($category, $filters['categories'])) {
+          $filtered[$name] = $project;
+        }
+      }
+    }
+    $projects = $filtered;
+  }
+
+  // Filter out projects based on the text query
+  if (isset($filters['text']) AND $text = $filters['text']) {
+    foreach ($projects as $name => $project) {
+      if (!stristr($project['title'], $text) AND !stristr($project['description'], $text)) {
+        unset($projects[$name]);
+      }
+    }
+  }
+  
+  $results['total'] = count($projects);
+  
+  // Only send back the requested amount
+  $start = $filters['page'] * $filters['requested'];
+  $end = $start + $filters['requested'];
+  
+  $results['projects'] = $projects;
+  
+  return $results;
+}
+
+/**
+ * Returns some static projects
+ */
+function project_browser_test_projects() {
+  $projects = array();
+  
+  $projects['views'] = array(
+    'type' => 'module',
+    'title' => 'Views',
+    'name' => 'views',
+    'drupal version' => 7,
+    'author' => 'merlinofchaos',
+    'description' => "The Views module provides a flexible method for Drupal site 
+      designers to control how lists and tables of content (nodes in Views 1, almost 
+      anything in Views 2) are presented. Traditionally, Drupal has hard-coded most of 
+      this, particularly in how taxonomy and tracker lists are formatted. ",
+    'drupal_versions' => array(6, 7),
+    'categories' => array('admin', 'search'),
+    'image' => 'http://learnbythedrop.com/system/files/images/View-Edit_0.png',
+    'usage' => '542312',
+    'project url' => 'http://www.drupal.org/projects/views',
+    'project status url' => 'http://updates.drupal.org/release-history/views/7.x',
+    'last updated' => '12342523',
+    'maintenance status' => 'Actively maintained',
+    'development status' => 'Under active development',
+    'rating' => '9.6',
+    'dependencies' => array(
+      'ctools',
+    ),
+  );
+  
+  $projects['ctools_test'] = array(
+    'type' => 'module',
+    'title' => 'CTools Test',
+    'name' => 'ctools_test',
+    'drupal version' => 7,
+    'author' => 'merlinofchaos',
+    'description' => "This suite is primarily a set of APIs and tools to improve 
+      the developer experience. It also contains a module called the Page Manager 
+      whose job is to manage pages. In particular it manages panel pages, but as 
+      it grows it will be able to manage far more than just Panels.",
+    'drupal_versions' => array(6, 7),
+    'categories' => array(),
+    'image' => '',
+    'usage' => '4312',
+    'project url' => 'http://www.drupal.org/projects/ctools',
+    'project status url' => 'http://updates.drupal.org/release-history/ctools/7.x',
+    'last updated' => '12354634',
+    'maintenance status' => 'Actively maintained',
+    'development status' => 'Under active development',
+    'rating' => '7.6',
+    'dependencies' => array(),
+  );
+  
+  $projects['ctools'] = array(
+    'type' => 'module',
+    'title' => 'Chaos Tool Suite',
+    'name' => 'ctools',
+    'drupal version' => 7,
+    'author' => 'merlinofchaos',
+    'description' => "This suite is primarily a set of APIs and tools to improve 
+      the developer experience. It also contains a module called the Page Manager 
+      whose job is to manage pages. In particular it manages panel pages, but as 
+      it grows it will be able to manage far more than just Panels.",
+    'drupal_versions' => array(6, 7),
+    'categories' => array(),
+    'image' => '',
+    'usage' => '4312',
+    'project url' => 'http://www.drupal.org/projects/ctools',
+    'project status url' => 'http://updates.drupal.org/release-history/ctools/7.x',
+    'last updated' => '12354634',
+    'maintenance status' => 'Actively maintained',
+    'development status' => 'Under active development',
+    'rating' => '7.6',
+    'dependencies' => array(),
+  );
+  
+  $projects['token'] = array(
+    'type' => 'module',
+    'title' => 'Token',
+    'name' => 'token',
+    'drupal version' => 7,
+    'author' => 'eaton',
+    'description' => "Tokens are small bits of text that can be placed into larger 
+      documents via simple placeholders, like %site-name or [user]. The Token module 
+      provides a central API for modules to use these tokens, and expose their own token values.",
+    'categories' => array('admin'),
+    'image' => 'http://drupal.org/files/images/token_08.thumbnail.png',
+    'usage' => '4563',
+    'project url' => 'http://www.drupal.org/projects/token',
+    'project status url' => 'http://updates.drupal.org/release-history/token/7.x',
+    'last updated' => '12357351',
+    'maintenance status' => 'Actively maintained',
+    'development status' => 'Under active development',
+    'rating' => '8.1',
+    'dependencies' => array(),
+  );
+  
+  $projects['zen'] = array(
+    'type' => 'theme',
+    'title' => 'Zen',
+    'name' => 'zen',
+    'drupal version' => 7,
+    'author' => 'johnAlbin',
+    'description' => "Zen is the ultimate starting theme for Drupal. If you are 
+      building your own standards-compliant theme, you will find it much easier to 
+      start with Zen than to start with Garland or Bluemarine. This theme has fantastic 
+      online documentation and tons of code comments for both the PHP (template.php) 
+      and HTML (page.tpl.php, node.tpl.php).",
+    'categories' => array('light', 'dark'),
+    'image' => 'http://drupal.org/files/images/zen-logo.thumbnail.png',
+    'usage' => '4563',
+    'project url' => 'http://www.drupal.org/project/zen',
+    'project status url' => 'http://updates.drupal.org/release-history/zen/7.x',
+    'last updated' => '12343634',
+    'maintenance status' => 'Actively maintained',
+    'development status' => 'Under active development',
+    'rating' => '7.1',
+    'dependencies' => array(),
+  );
+  
+  $projects['acquia_marina'] = array(
+    'type' => 'theme',
+    'title' => 'Acquia Marina',
+    'name' => 'acquia_marina',
+    'drupal version' => 7,
+    'author' => 'stephthegeek',
+    'description' => "The Fusion base theme and Skinr are required. Skinr for Drupal 7 
+      (dev release) is usable now but it is recommended that you proceed with caution 
+      and do some of your own testing.",
+    'categories' => array('light'),
+    'image' => 'http://drupal.org/files/images/acquia_marina.thumbnail.png',
+    'usage' => '14563',
+    'project url' => 'http://www.drupal.org/project/acquia_marina',
+    'project status url' => 'http://updates.drupal.org/release-history/acquia_marina/7.x',
+    'last updated' => '12346574',
+    'maintenance status' => 'Actively maintained',
+    'development status' => 'Under active development',
+    'rating' => '7.8',
+    'dependencies' => array(
+      'fusion'
+    ),
+  );
+  
+  $projects['fusion'] = array(
+    'type' => 'theme',
+    'title' => 'Fusion',
+    'name' => 'fusion',
+    'drupal version' => 7,
+    'author' => 'stephthegeek',
+    'description' => "Fusion is a powerful base theme, with layout and style configuration 
+      options built in that you can control through Drupal's UI. It's based on a simplified 
+      960px or fluid 12/16-column grid. It's designed to be used with the Skinr module, 
+      with numerous useful block styles included.",
+    'categories' => array('light'),
+    'image' => 'http://drupal.org/files/images/fusion-powering-small-banner.thumbnail.png',
+    'usage' => '14563',
+    'project url' => 'http://www.drupal.org/project/fusion',
+    'project status url' => 'http://updates.drupal.org/release-history/fusion/7.x',
+    'last updated' => '12342643',
+    'maintenance status' => 'Actively maintained',
+    'development status' => 'Under active development',
+    'rating' => '',
+    'dependencies' => array(),
+  );
+  
+  return $projects;
+}
diff --git a/core/modules/project_browser/theme/project-browser-block.tpl.php b/core/modules/project_browser/theme/project-browser-block.tpl.php
new file mode 100644
index 0000000..dc11389
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-block.tpl.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * @file
+ * Default theme implementation to display a block on the projects browser page
+ *
+ * Available variables:
+ * - $title: The title of the block
+ * - $content: The content of the block
+ * These are defined in project_browser_preprocess_project_browser_block()
+ * 
+ * @see project_browser_preprocess_project_browser_block()
+ */
+?>
+<div class="project_browser_block">
+  <?php print render($title_prefix); ?>
+  <h2><?php print $title; ?></h2>
+  <?php print render($title_suffix); ?>
+  <div class="content">
+    <?php print $content; ?>
+  </div>
+</div>
diff --git a/core/modules/project_browser/theme/project-browser-install-queue.tpl.php b/core/modules/project_browser/theme/project-browser-install-queue.tpl.php
new file mode 100644
index 0000000..81e018c
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-install-queue.tpl.php
@@ -0,0 +1,16 @@
+<?php 
+/**
+ * @file
+ * 
+ * Default theming implementation for displaying list of queued projects
+ * 
+ * Available variables:
+ * - $queue_html: The html for the install_queue 
+ * These are defined in project_browser_preprocess_project_browser_install_queue()
+ * 
+ * @see project_browser_preprocess_project_browser_install_queue()
+ */
+?>
+<div id="project-browser-install-queue" class="clearfix">
+  <?php print $queue_html; ?>
+</div>
diff --git a/core/modules/project_browser/theme/project-browser-install.tpl.php b/core/modules/project_browser/theme/project-browser-install.tpl.php
new file mode 100644
index 0000000..fb7771b
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-install.tpl.php
@@ -0,0 +1,21 @@
+<?php 
+/**
+ * @file
+ * 
+ * Default theming implementation for displaying the install page
+ * 
+ * Available variables:
+ * - $task_list: A list of tasks that are being performed, with the current task marked
+ * - $main_content: The html that goes in the center area of the page
+ * These are defined in project_browser_preprocess_project_browser_install()
+ * 
+ * @see project_browser_preprocess_project_browser_install()
+ */
+?>
+
+<div class="project-browser-install-sidebar-left">
+  <?php print $task_list; ?>
+</div>
+<div class="project-browser-install-main">
+  <?php print $main_content; ?>
+</div>
diff --git a/core/modules/project_browser/theme/project-browser-list.tpl.php b/core/modules/project_browser/theme/project-browser-list.tpl.php
new file mode 100644
index 0000000..6468dbd
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-list.tpl.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * @file
+ * 
+ * Default theming implementation for displaying list of projects.
+ * 
+ * Available variables:
+ * - $main_content: The main content area, namely the projects list, including the pager
+ * - $filters: The filters block
+ * - $install_list: The install queue block
+ * These are defined in project_browser_preprocess_project_browser_list()
+ * 
+ * @see project_browser_preprocess_project_browser_list()
+ */
+?>
+<div id="project-browser" class="clearfix">
+  <div id="project-browser-main" class="project-browser-region">
+    <?php print $main_content; ?>
+  </div>
+
+  <div id="project-browser-sidebar-right" class="project-browser-region">
+    <?php print $filters; ?>
+    <?php print $install_list; ?>
+  </div>
+  
+</div>
diff --git a/core/modules/project_browser/theme/project-browser-project.tpl.php b/core/modules/project_browser/theme/project-browser-project.tpl.php
new file mode 100644
index 0000000..ee77131
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-project.tpl.php
@@ -0,0 +1,59 @@
+<?php 
+/**
+ * @file
+ * 
+ * Default theme implementation for displaying a project.
+ * 
+ * Available variables:
+ * - $first: Whether or not this project item is the first one in the list
+ * - $image: The project image, if any
+ * - $title: The title of the project
+ * - $author: Ther author of the project
+ * - $last_updated: A string representing when the project was last updated
+ * - $description: The project description. This could be long.
+ * - $extras: A string of any extra info that should go at the bottom
+ * - $status: Whether the project is already installed, enabled, or not
+ * - $install: The add to install queue button/link
+ * These are defined in project_browser_preprocess_project_browser_project()
+ * 
+ * @see project_browser_preprocess_project_browser_project()
+ */
+
+$class = ($first) ? 'project-item-first' : 'project-item';
+ 
+?>
+<div class="<?php print $class; ?>">
+  <?php if ($image) { ?>
+    <div class='project-image'>
+      <img src='<?php print $image; ?>' />
+    </div>
+  <?php } ?>
+  
+  <div class="project-information" >
+    <div class="project-title">
+      <?php print $title; ?>
+    </div>
+    
+    <div class="project-author">
+      <?php print $author; ?>
+    </div>
+    
+    <div class="project-updated">
+      <?php print $last_updated; ?>
+    </div>
+    
+    <div class="project-description">
+      <?php print $description; ?>
+    </div>
+  </div>
+  
+  <div class="project-extra">
+    <?php print $extras; ?>
+  </div>
+  
+  <div class="project-status">
+      <?php print $status; ?>
+     	<?php print $install; ?>
+  </div>
+  <div class='clearfix'></div>
+</div>
diff --git a/core/modules/project_browser/theme/project_browser.admin.inc b/core/modules/project_browser/theme/project_browser.admin.inc
new file mode 100644
index 0000000..831b050
--- /dev/null
+++ b/core/modules/project_browser/theme/project_browser.admin.inc
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @file
+ * Project Browser module admin pages.
+ */
+
+// ======================================
+// Administration Page:
+// ======================================
+
+/**
+ * Admin Settings Form
+ */
+function project_browser_admin() {
+  $form['main'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Main settings'),
+    '#collapsible' => FALSE,
+    '#collapsed' => FALSE,
+  );
+  $form['main']['project_browser_servers'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Repositories'),
+    '#default_value' => variable_get('project_browser_servers', ''),
+    '#description' => t("Add new repositories to use for the Project Browser, one per line, in 
+      the 'url|method|Site Name' format. Drupal.org is added by default, and doesn't need to be 
+      set here."),
+    '#required' => FALSE,
+  );
+
+  return system_settings_form($form);
+}
-- 
1.7.3.1.msysgit.0

