diff --git a/core/modules/project_browser/config/project_browser.settings.yml b/core/modules/project_browser/config/project_browser.settings.yml
new file mode 100644
index 0000000..fd66849
--- /dev/null
+++ b/core/modules/project_browser/config/project_browser.settings.yml
@@ -0,0 +1,7 @@
+cache_lifetime: 86400
+# @todo - Change the link once drupal.org is ready.
+default_server:
+  http://drupal:drupal@pbs-drupal_7.redesign.devdrupal.org/project_browser/server:
+    name: Drupal.org
+    method: json
+project_browser_servers: ""
diff --git a/core/modules/project_browser/css/project_browser.admin.css b/core/modules/project_browser/css/project_browser.admin.css
new file mode 100644
index 0000000..0b4e08f
--- /dev/null
+++ b/core/modules/project_browser/css/project_browser.admin.css
@@ -0,0 +1,174 @@
+/* -------------- Project Browser ------------- */
+
+#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; /* LTR */
+  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;
+}
+
+/* ------------------ List Styles ------------------ */
+
+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; /* LTR */
+}
+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: #000000;
+}
+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; /* LTR */
+}
+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; /* LTR */
+}
+a.show-more {
+  float: right; /* LTR */
+}
+
+/* ------------------ Project ----------------- */
+
+.project-extra {
+  clear: both;
+  color: gray;
+  text-align: right; /* LTR */
+}
+.project-author {
+  color: gray;
+  font-size: 0.9em;
+}
+.project-updated {
+  color: gray;
+  font-size: 0.9em;
+}
+.project-image {
+  float: left; /* LTR */
+  margin-right: 10px; /* LTR */
+}
+.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; /* LTR */
+  text-align: right; /* LTR */
+  top: 5px;
+}
+div.project-information {
+
+}
+.project-browser-install-main {
+  padding: 10px;
+  float: right; /* LTR */
+  width: 76%;
+}
+.install-disabled {
+  color: gray;
+}
+.install-enabled {
+  color: green;
+}
+.project-browser-install-sidebar-left {
+  float: left; /* LTR */
+  width: 19%;
+  padding: 10px;
+}
+div.project-title {
+  font-size: 18px;
+}
+fieldset#edit-category,
+fieldset#edit-version  {
+  border: none;
+}
+div.install-item-prefix {
+  float:left; /* LTR */
+  margin-left: 50px; /* LTR */
+}
+
+/* -------------- Project Browser ------------- */
+
+#project-browser-main div.form-item-install {
+  float: right; /* LTR */
+}
+#project-browser div.project-browser-region {
+  min-height: 1px;
+}
+#project-browser div#project-browser-main {
+  width: 75%;
+  float: left; /* LTR */
+  margin-right: 1%; /* LTR */
+}
+#project-browser div#project-browser-sidebar-right {
+  width: 23%;
+  float: right; /* LTR */
+}
+#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/project_browser_more_link.js b/core/modules/project_browser/js/project_browser_more_link.js
new file mode 100644
index 0000000..c389cf1
--- /dev/null
+++ b/core/modules/project_browser/js/project_browser_more_link.js
@@ -0,0 +1,41 @@
+/**
+ * @file
+ * Makes the 'More' button toggle the display of the full project texts.
+ *
+ * The project descriptions are by default trimmed to a certain height. When
+ * the user clicks the more link, then the full text is shown.
+ */
+(function ($) {
+
+  "use strict";
+
+  Drupal.behaviors.projectBrowserMoreLink = {
+    attach: function (context, settings) {
+      // The height of the content block when it's not expanded
+      var adjustheight = 80;
+      // The "more" link text
+      var moreText = Drupal.t('More');
+      // The "less" link text
+      var lessText = Drupal.t('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_select_releases.js b/core/modules/project_browser/js/project_browser_select_releases.js
new file mode 100644
index 0000000..05a4614
--- /dev/null
+++ b/core/modules/project_browser/js/project_browser_select_releases.js
@@ -0,0 +1,22 @@
+/**
+ * @file
+ * Shows the default Select Releases page until 'Show All Releases' is clicked.
+ */
+(function ($) {
+
+  "use strict";
+
+  Drupal.behaviors.projectBrowserSelectReleases = {
+    attach: function (context, settings) {
+      $('.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/lib/Drupal/project_browser/Tests/ProjectBrowserTest.php b/core/modules/project_browser/lib/Drupal/project_browser/Tests/ProjectBrowserTest.php
new file mode 100644
index 0000000..9a4c855
--- /dev/null
+++ b/core/modules/project_browser/lib/Drupal/project_browser/Tests/ProjectBrowserTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\project_browser\Tests\ProjectBrowserTest.
+ */
+
+namespace Drupal\project_browser\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Administration tests for Project Browser.
+ */
+class ProjectBrowserTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('project_browser', 'project_browser_test');
+
+  /**
+   * User who will have access to use project browser.
+   *
+   * @var \Drupal\user\Plugin\Core\Entity\User
+   */
+  protected $privilegedUser;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Project Browser Install Project Test',
+      'description' => 'Attempts to install a project.',
+      'group' => 'Project Browser',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    // Set the default server variable.
+    $server_url = url('project_browser_test/query', array('absolute' => TRUE));
+    $config = config('project_browser.settings');
+    $config->set('default_server', array(
+      $server_url => array(
+        'name' => 'Test Server',
+        'method' => 'json',
+      ),
+    ));
+    $config->save();
+
+    // Create and log in our privileged user.
+    $this->privilegedUser = $this->drupalCreateUser(array(
+      'use project browser',
+    ));
+    $this->drupalLogin($this->privilegedUser);
+  }
+
+  /**
+   * Tests a search for the Token string, ensuring that token shows.
+   */
+  public function testProjectBrowserSearchToken() {
+    // Search for 'token'.
+    $edit = array();
+    $edit['search_text'] = 'token';
+    $this->drupalPost('admin/modules/project-browser/modules', $edit, t('Filter'));
+    $this->assertResponse(200);
+    $this->assertText('Showing 1 to');
+    $this->assertText('Tokens are small bits of text');
+  }
+
+  /**
+   * Tests that the Projects Listing displays properly with the default filters.
+   */
+  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->assertResponse(200);
+    $this->assertText('Showing 1 to');
+  }
+
+  /**
+   * Tests to make sure that the enabled project detection works.
+   */
+  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'), 'Make sure project enabled detection works.');
+  }
+
+  /**
+   * Tests that adding and removing items from the queue works properly.
+   */
+  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.');
+  }
+
+}
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..32ffcce
--- /dev/null
+++ b/core/modules/project_browser/project_browser.admin.inc
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Project Browser module admin pages.
+ */
+
+/**
+ * Form constructor for admin settings.
+ *
+ * @see project_browser_menu()
+ * @see project_browser_admin_form_submit()
+ * @ingroup forms
+ */
+function project_browser_admin_form($form, &$form_state) {
+  $form['main'] = array(
+    '#type' => 'details',
+    '#title' => t('Main settings'),
+    '#collapsed' => FALSE,
+  );
+  // Because this is a pluggable system, there can be other repositories besides
+  // Drupal.org.
+  $form['main']['project_browser_servers'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Repositories'),
+    '#default_value' => config('project_browser.settings')->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 $form;
+}
+
+/**
+ * Form submission handler for project_browser_admin_form().
+ */
+function project_browser_admin_form_submit($form, &$form_state) {
+  $config = config('project_browser.settings');
+  $config->set('project_browser_servers', $form_state['values']['project_browser_servers']);
+  $config->save();
+}
diff --git a/core/modules/project_browser/project_browser.inc b/core/modules/project_browser/project_browser.inc
new file mode 100644
index 0000000..195c2ac
--- /dev/null
+++ b/core/modules/project_browser/project_browser.inc
@@ -0,0 +1,1071 @@
+<?php
+
+/**
+ * @file
+ * Various functions that are required by project_browser.
+ */
+
+// @todo Remove these once http://drupal.org/node/1846078 is committed.
+use Drupal\Core\Updater\Updater;
+use Drupal\Core\FileTransfer\Local;
+
+/**
+ * Builds the themed install list form.
+ *
+ * @return array
+ *   A render array for the install queue block.
+ */
+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));
+}
+
+/**
+ * Form constructor for the install button for the Install Queue block.
+ *
+ * Since the selected projects are stored in the $_SESSION variable, no real
+ * processing is done, we just redirect to the install/select_versions page.
+ *
+ * @see project_browser_preprocess_project_browser_install_queue()
+ * @ingroup forms
+ */
+function project_browser_install_button_form($form, &$form_state) {
+  $form['#attributes']['id'] = 'project-browser-install-button-form';
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => 't(Install)',
+  );
+
+  $form['#action'] = url('admin/modules/project-browser/install/select_versions');
+
+  return $form;
+}
+
+/**
+ * Form constructor for the filters form.
+ *
+ * This includes categories and the string search box, and the $type is stored.
+ *
+ * @param string $type
+ *   The type of project (module or theme).
+ *
+ * @see project_browser_filters_form_submit()
+ * @ingroup forms
+ */
+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 category filter if there are categories.
+  if ($categories = project_browser_get_categories($type)) {
+    array_unshift($categories, t('-- Any --'));
+    $form['category'] = array(
+      '#type' => 'select',
+      '#title' => t('Category'),
+      '#options' => $categories,
+      '#default_value' => isset($_SESSION['project_browser_category_filter_' . $type]) ? $_SESSION['project_browser_category_filter_' . $type] : NULL,
+    );
+  }
+
+  $form['project_type'] = array(
+    '#type' => 'value',
+    '#value' => $type,
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Filter'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submission handler for project_browser_filters_form().
+ *
+ * All that we do here is store the selected category and search string in the
+ * $_SESSION variable.
+ */
+function project_browser_filters_form_submit($form, &$form_state) {
+  $type = $form_state['values']['project_type'];
+  if (!empty($form_state['values']['category'])) {
+    $_SESSION['project_browser_category_filter_' . $type] = array($form_state['values']['category'] => $form_state['values']['category']);
+  }
+  else {
+    $_SESSION['project_browser_category_filter_' . $type] = array();
+  }
+  $_SESSION['project_browser_text_filter_' . $type] = $form_state['values']['search_text'];
+}
+
+/**
+ * Builds a themed sort widget for the results.
+ *
+ * These are links which can be clicked/toggled to select and change direction.
+ *
+ * @param array $sort_options
+ *   An array of sort options.
+ * @param string $current_sort_option
+ *   The currently selected sort option, which is a string.
+ * @param string $current_sort_direction
+ *   The currently selected sort direction, which is a string.
+ *
+ * @return string
+ *   A themed list of sort options, as a string.
+ */
+function project_browser_get_sort_widget(array $sort_options, $current_sort_option, $current_sort_direction) {
+  $sort_list = array();
+  $sort_list[] = array(
+    '#markup' => t('Sort by:'),
+    '#wrapper_attributes' => array(
+      'class' => array('sort-header'),
+    ),
+  );
+
+  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_sort_option == $sort_option['method']) {
+      $classes[] = 'sort-active';
+      $classes[] = 'sort-' . $current_sort_direction;
+
+      // Set the direction of the sort link to the opposite of what it currently
+      // is.
+      if ($current_sort_direction == $query['sort']) {
+        if ($query['sort'] == 'desc') {
+          $query['sort'] = 'asc';
+        }
+        else {
+          $query['sort'] = 'desc';
+        }
+      }
+    }
+    else {
+      $classes[] = 'sort-inactive';
+    }
+    $sort_list[] = array(
+      '#type' => 'link',
+      '#title' => $sort_option['name'],
+      '#href' => current_path(),
+      '#options' => array('query' => $query, 'class' => array()),
+      '#wrapper_attributes' => array(
+        'class' => $classes,
+      ),
+    );
+  }
+
+  // @todo Should return a render array.
+  return array(
+    '#theme' => 'item_list',
+    '#items' => $sort_list,
+    '#type' => 'ul',
+    '#attributes' => array('class' => array('project-browser-sort-list')),
+  );
+}
+
+/**
+ * Builds a themed widget to select the server.
+ *
+ * This is only called if there are more than one server enabled in the
+ * settings.
+ *
+ * @param array $servers
+ *   An array of servers that should be available as options.
+ * @param string $current_server
+ *   The currently selected server, which is a string.
+ *
+ * @return string
+ *   A themed server select widget, as a string.
+ */
+function project_browser_get_server_widget(array $servers, $current_server) {
+  $list = array();
+  $list[] = array(
+    'data' => t('Repository:'),
+    'class' => array('server-header')
+  );
+  $current_path = drupal_get_path_alias($_GET['q']);
+
+  // @todo Improve comment or variable name of $i.
+  // Interate through the repository number and current server number.
+  $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(
+      '#type' => 'link',
+      '#title' => $server['name'],
+      '#href' => $current_path,
+      '#options' => array('query' => $query, 'class' => array()),
+      '#wrapper_attributes' => array(
+        'class' => $classes,
+      ),
+    );
+
+    $i += 1;
+  }
+
+  // @todo Should return a render array.
+  return array(
+    '#theme' => 'item_list',
+    '#items' => $list,
+    '#type' => 'ul',
+    '#attributes' => array('class' => array('project-browser-servers-list')),
+  );
+}
+
+/**
+ * Builds and returns an array of sort options, keyed by method.
+ *
+ * @param bool $full
+ *   (optional) Set this to TRUE if you want to get all of the supported sort
+ *   methods. Defaults to FALSE.
+ *
+ * @return array
+ *   An array of sort options, keyed by method.
+ */
+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;
+}
+
+/**
+ * Builds an array of all available categories for a project type.
+ *
+ * @param string $type
+ *   The type of project to get the categories for. Example: 'module' or
+ *   'theme'.
+ *
+ * @return array|null
+ *   Array containing all available categories or NULL if no categories.
+ */
+function project_browser_get_categories($type) {
+  $categories = array();
+
+  // Get which server to use from $_SESSION.
+  $use_server = isset($_SESSION['project_browser_server_filter']) ? $_SESSION['project_browser_server_filter'] : 0;
+
+  $categories_raw = project_browser_fetch_categories($type, $use_server);
+
+  if (is_array($categories_raw) && !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) && !empty($categories)) {
+    ksort($categories);
+
+    return $categories;
+  }
+  return NULL;
+}
+
+/**
+ * Prepares the categories for sending to the servers as filters.
+ *
+ * @param array $raw_cats
+ *   An array of categories from $form_state['values'].
+ * @param string $type
+ *   The type of project for which to prepare the categories, as a string, for
+ *   example, string 'module' or 'theme'.
+ *
+ * @return array
+ *   An array of server categories, keyed by server url.
+ */
+function project_browser_prepare_categories(array $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]) && $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 string $type
+ *   The type of project, as a string. For example, could be 'theme' or
+ *   'module'.
+ * @param string $name
+ *   The short name of the project, as a string.
+ *
+ * @return bool
+ *   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.
+ *
+ * @return array
+ *   An array of listed projects from the $_SESSION variable.
+ */
+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 variable.
+ *
+ * @param string|null $type
+ *   (optional) The type of project (module or theme). Defaults to NULL, which
+ *   will return projects of all types.
+ *
+ * @return array
+ *   An array of projects that are queued for install.
+ */
+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) && !empty($project)) {
+        if (isset($type) && $type != $project['type']) {
+          continue;
+        }
+        else {
+          $projects[$project['name']] = $project;
+        }
+      }
+    }
+  }
+
+  return $projects;
+}
+
+/**
+ * Gets a release from a project and a release_name.
+ *
+ * @param string $release_name
+ *   The name of the release, such as '7.x-1.2'.
+ * @param array $project
+ *   The $project data array.
+ *
+ * @return array|null
+ *   The release data array or NULL if the release doesn't exist.
+ */
+function project_browser_get_release($release_name, array $project) {
+  $release_data = project_browser_get_project_release_data($project);
+
+  return isset($release_data['releases'][$release_name]) ? $release_data['releases'][$release_name] : NULL;
+}
+
+/**
+ * Gets the newly installed projects from the session.
+ *
+ * @return array
+ *   An array of all of the newly installed projects.
+ */
+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) && !empty($project)) {
+        $projects[$project['name']] = $project;
+      }
+    }
+  }
+
+  return $projects;
+}
+
+/**
+ * Adds a project to the install queue $_SESSION variable.
+ *
+ * @param array $project
+ *   An array of $project data for a single project.
+ */
+function project_browser_install_queue_add(array $project) {
+  $_SESSION['project_browser_install_list'][$project['name']] = $project;
+}
+
+/**
+ * Removes a project from the install queue $_SESSION variable.
+ *
+ * @param string $project_name
+ *   The name of the project to remove, as a string, such as 'views'.
+ */
+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 variable.
+ *
+ * @return array
+ *   An array of the currently selected releases.
+ */
+function project_browser_get_queued_releases() {
+  $releases = array();
+
+  if (isset($_SESSION['project_browser_install_releases_list'])) {
+    foreach ($_SESSION['project_browser_install_releases_list'] as $short_name => $info) {
+      if (is_array($info['project']) && !empty($info['project'])) {
+        $releases[$short_name] = $info;
+      }
+    }
+  }
+
+  return $releases;
+}
+
+/**
+ * Fetches results from the servers based on the parameters passed in.
+ *
+ * @param array $filters
+ *   An associative array of queries to use to filter results, containing:
+ *   - version: The Major Version of Drupal that is running on the Client.
+ *     Example: '7' or '8'.
+ *   - text: The text that was entered as the search query, or '' if none.
+ *     Example: 'Link field'.
+ *   - categories: An array of categories that were selected, if any.
+ *   - type: The type of project being searched for. Example: 'module' or
+ *     'theme'.
+ *   - page: The zero-based page number.
+ *   - requested: How many results are requested per page.
+ *
+ * @return array
+ *   An array of results formatted like:
+ *   - total: The total number of results found for the filters.
+ *   - projects: An array of projects returned for this page request,
+ *     containing:
+ *     - KEY: A project array keyed by the machine name:
+ *       - type: The type of project this is. Can be 'module' or 'theme'.
+ *       - title: The title of the project.
+ *       - name: The machine name of the project.
+ *       - author: The author's name.
+ *       - description: Long project description text.
+ *       - image: Absolute url to the image, if any.
+ *       - usage: How many sites are using module.
+ *       - project url: Absolute url to the project page, if any.
+ *       - project status url: The absolute url of the update checker,
+ *         formatted like how Drupal.org Update Status does it.
+ *       - last updated: UNIX Timestamp of when the project was last updated.
+ *       - maintenance status: Maintenance status.
+ *       - development status: Development status.
+ *       - rating: A rating on a scale of 1 to 10 of the project, if available
+ *       - dependencies: An array of the dependencies of this module, by
+ *         project shortname, if available.
+ */
+function project_browser_fetch_results(array $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() && $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 (!empty($results_raw['projects']) && is_array($results_raw['projects'])) {
+      // Merge the results.
+      $results['projects'] = array_merge($results['projects'], $results_raw['projects']);
+    }
+  }
+
+  cache()->set($cid, $results, REQUEST_TIME + config('project_browser.settings')->get('cache_lifetime'));
+
+  return $results;
+}
+
+/**
+ * Fetches categories from the servers based on the type of project.
+ *
+ * @param string $type
+ *   The type of project we are getting categories for, a string, which can be
+ *   'module' or 'theme'.
+ * @param string $use_server
+ *   (optional) The server to use, which is a string. Defaults to 'all'.
+ *
+ * @return array
+ *   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) {
+      $categories_raw = array();
+      // 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() && $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) && !empty($categories_raw)) {
+        $categories[$url] = $categories_raw;
+      }
+    }
+
+    // Cache this for 24 hours.
+    cache()->set($cid, $categories, REQUEST_TIME + config('project_browser.settings')->get('cache_lifetime'));
+  }
+
+  return $categories;
+}
+
+/**
+ * Gets an array of servers to use for fetching results.
+ *
+ * @param string $use_server
+ *   (optional) The server to use, which is a string. Defaults to 'all'.
+ *
+ * @return array
+ *   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') {
+  $servers = config('project_browser.settings')->get('default_server');
+
+  if ($servers_raw = config('project_browser.settings')->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.
+  // @todo Improve comment or variable name of $i.
+  // Interate through the server number.
+  if ($use_server !== 'all') {
+    $i = 0;
+    foreach ($servers as $url => $server) {
+      if ($use_server != $i) {
+        unset($servers[$url]);
+      }
+      $i += 1;
+    }
+  }
+
+  return $servers;
+}
+
+/**
+ * Gets the available releases based on project status url.
+ *
+ * @param array $project
+ *   An array with inforamation for the project to get the releases for.
+ *
+ * @return array|false
+ *   An array of releases for this project, or FALSE if it can't be found.
+ */
+function project_browser_get_project_release_data(array $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) && isset($data->data['releases']) && is_array($data->data['releases'])) {
+      return $data->data;
+    }
+  }
+
+  return FALSE;
+}
+
+/**
+ * Downloads and installs a project using the update module.
+ *
+ * @todo Use this new method once the patch in
+ *   http://drupal.org/node/1846078 is committed.
+ *
+ * @param string $url
+ *   A string for the url of the release download.
+ *
+ * @return array
+ *   An array indicating whether or not this was successful, and an error
+ *   message if applicable.
+ */
+
+// @todo When will this fuction be uncommented?
+/**
+function project_browser_download_project($url) {
+  module_load_include('inc', 'update', 'update.manager');
+  module_load_include('inc', 'update', 'update.authorize');
+  // 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 {
+    if ($arguments = update_manager_install_project($local_cache)) {
+      extract($arguments);
+      try {
+        $context = array();
+        watchdog('debug', '<pre>' . print_r($arguments, TRUE) . '</pre>');
+        update_authorize_batch_copy_project($project, $updater_name, $local_url, $filetransfer, $context);
+        return array(
+          'success' => TRUE,
+        );
+      }
+      catch (UpdaterException $e) {
+        return array(
+          'success' => FALSE,
+          'message' => $e->getMessage(),
+        );
+      }
+    }
+    else {
+      return array(
+        'success' => FALSE,
+        'message' => t('The project could not be installed.'),
+      );
+    }
+  }
+  catch (Exception $e) {
+    return array(
+      'success' => FALSE,
+      'message' => $e->getMessage(),
+    );
+  }
+}
+*/
+
+/**
+ * Downloads and installs a project using the update module.
+ *
+ * @param string $url
+ *   A string for the url of the release download.
+ *
+ * @return array
+ *   An array indicating whether or not this was successful, and an error
+ *   message if applicable.
+ */
+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),
+    );
+  }
+
+  // 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.
+ *
+ * @param string $release_name
+ *   The name of the release, as a string, for example, '7.x-1.2'.
+ * @param array $project
+ *   The project data array.
+ * @param array &$context
+ *   The context of the batch so that the results can be reported.
+ */
+function _project_browser_batch_install_release($release_name, array $project, array &$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 finishes up the batch.
+ *
+ * If there were any errors, they are reported here with drupal_set_message().
+ * The user is then redirected to the select versions page if there were errors,
+ * the install dependencies page if there were any detected missing
+ * dependencies, or the enable modules page if there were no errors.
+ *
+ * @param bool $success
+ *   Whether or not the whole operation was successful.
+ * @param array $results
+ *   An array of messages about any failures.
+ * @param array $operations
+ *   An array of operations that need to be performed.
+ */
+function _project_browser_batch_install_releases_finished($success, array $results, array $operations) {
+  drupal_get_messages();
+
+  // Restore the maintenance mode to what it was at the start.
+  if (isset($_SESSION['maintenance_mode'])) {
+    config('system.maintenance')->set('enabled', $_SESSION['maintenance_mode'])->save();
+    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.
+ *
+ * @param array $projects
+ *   An array of projects to get the missing dependencies for.
+ *
+ * @return array
+ *   An array of missing dependencies, if any were detected.
+ */
+function project_browser_get_missing_dependencies(array $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..343c840
--- /dev/null
+++ b/core/modules/project_browser/project_browser.info
@@ -0,0 +1,7 @@
+name = Project Browser
+description = Allows users to browse for and install modules and themes.
+dependencies[] = update
+package = Core
+version = VERSION
+core = 8.x
+configure = admin/config/development/project_browser
diff --git a/core/modules/project_browser/project_browser.module b/core/modules/project_browser/project_browser.module
new file mode 100644
index 0000000..1550158
--- /dev/null
+++ b/core/modules/project_browser/project_browser.module
@@ -0,0 +1,536 @@
+<?php
+
+/**
+ * @file
+ * Project Browser module.
+ *
+ * This module provides a new UI for admins to easily browse modules and themes
+ * from their admin pages, and install them.
+ */
+
+/**
+ * Implements hook_help().
+ */
+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 hook_permission().
+ */
+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 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: Allows for adding to and removing from the install queue.
+ *
+ * This is invoked via AJAX most of the time.
+ *
+ * @param string $method
+ *   The method used for this callback.
+ * @param string $op
+ *   The operation to perform.
+ * @param string $project_name
+ *   The short name of the project.
+ *
+ * @return array
+ *   An array of elements that should be changed.
+ *
+ * @see project_browser_menu()
+ */
+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().
+ */
+function project_browser_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+  switch ($root_path) {
+    // This is used to put the 'Project Browser' action on the 'Modules' page.
+    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;
+
+    // This is used to include the old 'Install a module' link
+    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;
+
+    // This is used to put the 'Project Browser' action on the 'Appearance'
+    // page.
+    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;
+  }
+}
+
+/**
+ * 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',
+    ),
+  );
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for project-browser-install.tpl.php.
+ *
+ * Adds some variables for the projects install theme.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *     - current_task: The current task.
+ *
+ * @ingroup themeable
+ */
+function project_browser_preprocess_project_browser_install(array &$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']);
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for project-browser-install-queue.tpl.php.
+ *
+ * Adds some variables for the projects install queue theme.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *     - projects: An array of projects in the install queue.
+ *
+ * @ingroup themeable
+ */
+function project_browser_preprocess_project_browser_install_queue(array &$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);
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for project-browser-block.tpl.php.
+ *
+ * Adds some variables for the project browser block theme.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *     - element['#title']: The title of the block.
+ *     - element['#content']: The content of the block.
+ *
+ * @ingroup themeable
+ */
+function project_browser_preprocess_project_browser_block(array &$variables) {
+  // Add the title and content variables.
+  $variables['title'] = $variables['element']['#title'];
+  $variables['content'] = $variables['element']['#content'];
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for project-browser-list.tpl.php.
+ *
+ * Adds some variables for the projects list theme.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *     - projects_list: An array of all projects.
+ *
+ * @ingroup themeable
+ */
+function project_browser_preprocess_project_browser_list(array &$variables) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  drupal_add_library('project_browser', 'drupal.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('Most used Drupal modules');
+      break;
+
+    case 'theme':
+      $title = t('Most used Drupal themes');
+      break;
+
+    default:
+      $title = t('Projects');
+      break;
+
+  }
+
+  // If any filters were selected, change the title to 'Search results'.
+  if (!empty($_SESSION['project_browser_text_filter_' . $variables['type']]) || !empty($_SESSION['project_browser_category_filter_' . $variables['type']])) {
+    $title = t('Search results');
+  }
+
+  $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);
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for project-browser-project.tpl.php.
+ *
+ * Adds some variables for the project theme.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *     - project: An associative array of project variables.
+ *
+ * @ingroup themeable
+ */
+function project_browser_preprocess_project_browser_project(array &$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.
+ *
+ * @param string $project_name
+ *   The short name of the project, such as 'views'.
+ * @param string|null $title
+ *   (optional) The title of the project. Defaults to NULL.
+ * @param string $id_prefix
+ *   (optional) The prefix that should be prepended to the id, to ensure unique
+ *   id names. Defaults to 'add-to-queue-link'.
+ *
+ * @return string
+ *   A themed link to remove or add an item from the install queue.
+ */
+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_info().
+ */
+function project_browser_library_info() {
+  $libraries['drupal.project_browser'] = array(
+    'title' => 'Project Browser',
+    'version' => VERSION,
+    'js' => array(
+      drupal_get_path('module', 'project_browser') . '/js/project_browser_more_link.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('project_browser', 'drupal.project_browser.css'),
+    ),
+  );
+  $libraries['drupal.project_browser.select_releases'] = array(
+    'title' => 'Project Browser Select Releases',
+    'version' => VERSION,
+    'js' => array(
+      drupal_get_path('module', 'project_browser') . '/js/project_browser_select_releases.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('project_browser', 'drupal.project_browser.css'),
+    ),
+  );
+  $libraries['drupal.project_browser.css'] = array(
+    'title' => 'Project Browser CSS',
+    'version' => VERSION,
+    'css' => array(
+      drupal_get_path('module', 'project_browser') . '/css/project_browser.admin.css' => array(
+        'type' => 'file',
+        'media' => 'screen',
+      ),
+    ),
+  );
+  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..0c71028
--- /dev/null
+++ b/core/modules/project_browser/project_browser.pages.inc
@@ -0,0 +1,565 @@
+<?php
+
+/**
+ * @file
+ * This contains most of the page callbacks for the project browser module.
+ */
+
+/**
+ * Page callback: Displays the projects from a query.
+ *
+ * This is used on both the admin/modules/project-browser/modules and
+ * admin/modules/project-browser/themes pages to show a list of projects that
+ * can be installed and searched through, fetched from the server(s).
+ *
+ * @param string $type
+ *   Type of project to view, ie 'module' or 'theme'.
+ *
+ * @return array
+ *   A render array for a page containing a list of projects and filters.
+ *
+ * @see project_browser_menu()
+ */
+function project_browser_page($type) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+
+  // Set any filters in the session that are needed.
+  if (!empty($_GET['repository'])) {
+    $_SESSION['project_browser_sever_filter'] = $_GET['repository'];
+  }
+  if (!empty($_GET['order_by'])) {
+    $_SESSION['project_browser_order_by_filter_' . $type] = $_GET['order_by'];
+  }
+  if (!empty($_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']),
+    );
+  }
+
+  $build['#attached']['library'][] = array(
+    'project_browser',
+    'drupal.project_browser'
+  );
+
+  return $build;
+}
+
+/**
+ * Builds a page from the install process.
+ *
+ * @param string $op
+ *   Operation to perform.
+ *
+ * @return string
+ *   A themed page from the install process, depending on the $op.
+ *
+ * @see project_browser_menu()
+ */
+function project_browser_installation_page($op) {
+  drupal_add_library('project_browser', 'drupal.project_browser.css');
+
+  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)));
+}
+
+/**
+ * Page callback: Shows the Select versions installation task.
+ *
+ * Shows a form where the user can select which versions to install for each
+ * project.
+ *
+ * @return array
+ *   The form to select the versions of the projects the user wants to install.
+ *
+ * @see project_browser_menu()
+ */
+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 constructor for the select versions form.
+ *
+ * @param array $projects
+ *   An array of projects to get the releases for.
+ *
+ * @see project_browser_installation_select_versions_form_submit()
+ * @ingroup forms
+ */
+function project_browser_installation_select_versions_form($form, &$form_state, array $projects) {
+  module_load_include('inc', 'project_browser', 'project_browser');
+  drupal_add_library('project_browser', 'drupal.project_browser.select_releases');
+
+  $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;
+}
+
+/**
+ * Form submission handler for project_browser_installation_select_versions_form().
+ *
+ * This sets the batch to install the different selected releases one by one.
+ */
+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'] = config('system.maintenance')->get('enabled');
+  if ($form_state['values']['maintenance_mode'] == TRUE) {
+    config('system.maintenance')->set('enabled', TRUE)->save();
+  }
+
+  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['project']['name']] = array(
+        'release_name' => $item['release_name'],
+        'project' => $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 $short_name => $info) {
+    $operations[] = array('_project_browser_batch_install_release', array($info['release_name'], $info['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);
+}
+
+/**
+ * Page callback: Handles the Install Dependencies installation task.
+ *
+ * This shows a form which lets the user select which version of dependencies
+ * to install. This is only shown if there are missing dependencies. If there
+ * are no missing dependencies, then we redirect to the enable page.
+ *
+ * @return array
+ *   The form array to let the user select the dependencies to install.
+ *
+ * @see project_browser_menu()
+ */
+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,
+          // Missing dependencies only works for projects of type 'module'
+          'type' => 'module',
+          'title' => $shortname,
+        );
+      }
+    }
+
+    return drupal_get_form('project_browser_installation_select_versions_form', $missing_projects);
+  }
+  else {
+    drupal_goto('admin/modules/project-browser/install/enable');
+  }
+}
+
+/**
+ * Page callback: Shows the options for the Enable projects installation task.
+ *
+ * This shows a form which lets the user enable the newly installed projects. If
+ * there are unresolved dependencies, then the project is shown with a message
+ * about why it can't be enabled. This redirects to the project-browser page if
+ * there were no installed projects.
+ *
+ * @return array
+ *   The form array to enable projects, or redirect to
+ *   'admin/modules/project-browser'.
+ *
+ * @see project_browser_menu()
+ */
+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 constructor for the enable projects form.
+ *
+ * @param array $projects
+ *   An array of newly installed projects to enable.
+ *
+ * @see project_browser_installation_enable_form_submit()
+ * @ingroup forms
+ */
+function project_browser_installation_enable_form($form, &$form_state, array $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 submission handler for project_browser_installation_enable_form().
+ *
+ * Enables the selected projects from the enable projects form. After the
+ * selected projects are enabled, we flush all caches and then redirect to the
+ * modules page.
+ */
+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);
+
+  drupal_flush_all_caches();
+
+  drupal_goto('admin/modules');
+}
+
+/**
+ * Builds a task list to the sidebar area when installing projects.
+ *
+ * This will need to be called from every page of the install process.
+ *
+ * @param string $active
+ *   (optional) Set the active task by key. Defaults to NULL.
+ *
+ * @return array
+ *   The themed task list for the install projects process.
+ */
+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/tests/project_browser_test.info b/core/modules/project_browser/tests/project_browser_test.info
new file mode 100644
index 0000000..543e1d0
--- /dev/null
+++ b/core/modules/project_browser/tests/project_browser_test.info
@@ -0,0 +1,6 @@
+name = Project Browser module tests
+description = Support module for Project Browser related testing.
+package = Testing
+version = VERSION
+core = 8.x
+hidden = TRUE
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..46b456f
--- /dev/null
+++ b/core/modules/project_browser/tests/project_browser_test.module
@@ -0,0 +1,343 @@
+<?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.
+ *
+ * @see project_browser_test_menu()
+ */
+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_get_categories($_GET['type']);
+
+      print drupal_json_encode($categories);
+      exit();
+
+    case 'query':
+      // Check that we have valid data.
+      if (!isset($_GET['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['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 an array of static categories to use for testing.
+ *
+ * @param string $type
+ *   The type of project, 'module' or 'theme'.
+ *
+ * @return array
+ *   An array of categories for the project type.
+ */
+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 an array of filtered projects.
+ *
+ * @param array $filters
+ *   An array of filters to use.
+ *
+ * @return array
+ *   An array of projects.
+ */
+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 an array of projects to use for testing.
+ *
+ * @return array
+ *   An array of projects.
+ */
+function project_browser_test_projects() {
+  $projects = array();
+
+  $projects['views'] = array(
+    'type' => 'module',
+    'title' => 'Views',
+    'name' => 'views',
+    'drupal version' => 8,
+    '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. ",
+    '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' => 8,
+    '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.",
+    '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' => 8,
+    '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.",
+    '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' => 8,
+    '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' => 8,
+    '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' => 8,
+    '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' => 8,
+    '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..e5286dd
--- /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.
+ *
+ * @see project_browser_preprocess_project_browser_block()
+ * @ingroup themeable
+ */
+?>
+<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..4d611ea
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-install-queue.tpl.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * @file
+ * Default theming implementation for displaying list of queued projects.
+ *
+ * Available variables:
+ * - $queue_html: The html for the install_queue.
+ *
+ * @see project_browser_preprocess_project_browser_install_queue()
+ * @ingroup themeable
+ */
+?>
+<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..98abc9c
--- /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.
+ *
+ * @see project_browser_preprocess_project_browser_install()
+ * @ingroup themeable
+ */
+?>
+
+<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..6e0132f
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-list.tpl.php
@@ -0,0 +1,24 @@
+<?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.
+ *
+ * @see project_browser_preprocess_project_browser_list()
+ * @ingroup themeable
+ */
+?>
+<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..3d5f484
--- /dev/null
+++ b/core/modules/project_browser/theme/project-browser-project.tpl.php
@@ -0,0 +1,58 @@
+<?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.
+ *
+ * @see project_browser_preprocess_project_browser_project()
+ * @ingroup themeable
+ */
+
+$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" >
+    <h3 class="project-title">
+      <?php print $title; ?>
+    </h3>
+
+    <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>
