[gnome-boxes/boxes-newbox-assistant-306] assistant: Introduce the new Assistant/Download manager



commit 2d2ea7cf2cff1c0aa3b6ccbd8ef07411447ff9c5
Author: Felipe Borges <felipeborges gnome org>
Date:   Mon Nov 4 10:00:33 2019 +0100

    assistant: Introduce the new Assistant/Download manager
    
    Fixes #306
    Fixes #410
    Fixes #409

 data/gnome-boxes.gresource.xml              |   9 +
 data/gtk-style.css                          |  18 +-
 data/ui/assistant/pages/downloads-page.ui   | 139 +++++++++++++
 data/ui/assistant/pages/index-page.ui       | 305 ++++++++++++++++++++++++++++
 data/ui/assistant/pages/preparation-page.ui |  84 ++++++++
 data/ui/assistant/pages/review-page.ui      | 108 ++++++++++
 data/ui/assistant/pages/setup-page.ui       |  21 ++
 data/ui/assistant/rhel-download-dialog.ui   |  45 ++++
 data/ui/assistant/vm-assistant.ui           |  65 ++++++
 data/ui/collection-toolbar.ui               |  29 ++-
 data/ui/downloads-hub-row.ui                |  70 +++++++
 data/ui/downloads-hub.ui                    |  16 ++
 data/ui/wizard-downloadable-entry.ui        |   3 -
 data/ui/wizard-media-entry.ui               |   5 +-
 src/app-window.vala                         |   6 +-
 src/app.vala                                |  10 +-
 src/assistant/assistant-page.vala           |  17 ++
 src/assistant/downloads-page.vala           | 118 +++++++++++
 src/assistant/index-page.vala               | 159 +++++++++++++++
 src/assistant/preparation-page.vala         |  69 +++++++
 src/assistant/review-page.vala              | 123 +++++++++++
 src/assistant/rhel-download-dialog.vala     | 182 +++++++++++++++++
 src/assistant/setup-page.vala               |  34 ++++
 src/assistant/vm-assistant.vala             | 149 ++++++++++++++
 src/collection-toolbar.vala                 |  12 +-
 src/downloads-hub.vala                      | 129 ++++++++++++
 src/meson.build                             |   9 +
 src/util-app.vala                           |  16 ++
 src/vm-creator.vala                         |   2 +-
 29 files changed, 1935 insertions(+), 17 deletions(-)
---
diff --git a/data/gnome-boxes.gresource.xml b/data/gnome-boxes.gresource.xml
index c7ba7de1..4db4ad80 100644
--- a/data/gnome-boxes.gresource.xml
+++ b/data/gnome-boxes.gresource.xml
@@ -13,6 +13,8 @@
     <file preprocess="xml-stripblanks">ui/collection-toolbar.ui</file>
     <file preprocess="xml-stripblanks">ui/display-page.ui</file>
     <file preprocess="xml-stripblanks">ui/display-toolbar.ui</file>
+    <file preprocess="xml-stripblanks">ui/downloads-hub.ui</file>
+    <file preprocess="xml-stripblanks">ui/downloads-hub-row.ui</file>
     <file preprocess="xml-stripblanks">ui/editable-entry.ui</file>
     <file preprocess="xml-stripblanks">ui/empty-boxes.ui</file>
     <file preprocess="xml-stripblanks">ui/icon-view.ui</file>
@@ -48,5 +50,12 @@
     <file preprocess="xml-stripblanks">ui/wizard-web-view.ui</file>
     <file preprocess="xml-stripblanks">ui/wizard-window.ui</file>
     <file preprocess="xml-stripblanks">ui/assistant/remote-connection.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/vm-assistant.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/rhel-download-dialog.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/index-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/downloads-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/preparation-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/setup-page.ui</file>
+    <file preprocess="xml-stripblanks">ui/assistant/pages/review-page.ui</file>
   </gresource>
 </gresources>
diff --git a/data/gtk-style.css b/data/gtk-style.css
index 40f0391d..622f14d4 100644
--- a/data/gtk-style.css
+++ b/data/gtk-style.css
@@ -43,6 +43,10 @@
     border: 1px solid @theme_bg_color;
 }
 
+.bold-label {
+    font-weight: bold;
+}
+
 /******************
  * New Box Wizard *
  ******************/
@@ -81,18 +85,24 @@
 }
 
 .boxes-menu-row {
-    background: none;
-    border: 1px solid @borders;
+    background: @theme_unfocused_base_color;
 }
 
 .boxes-menu-row:hover {
     background-color: @theme_bg_color;
 }
 
-.boxes-menu-subrow {
-    padding: 10px;
+.sources-list {
+    border-radius: 5px;
 }
 
+separator {
+    background-color: @borders;
+}
+
+.list-expand-button {
+    border-top: 1px solid @borders;
+}
 
 /* Adds a border to the ISOs lists top undershoot */
 .boxes-menu-scrolled.undershoot.top { border-top: 1px solid @borders; }
diff --git a/data/ui/assistant/pages/downloads-page.ui b/data/ui/assistant/pages/downloads-page.ui
new file mode 100644
index 00000000..2219a4ac
--- /dev/null
+++ b/data/ui/assistant/pages/downloads-page.ui
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.9 -->
+  <template class="BoxesAssistantDownloadsPage" parent="GtkStack">
+    <signal name="key-press-event" handler="on_key_pressed"/>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="border-width">20</property>
+        <property name="margin-start">20</property>
+        <property name="margin-end">20</property>
+        <style>
+          <class name="linked"/>
+          <class name="frame"/>
+          <class name="sources-list"/>
+        </style>
+
+        <child>
+          <object class="GtkListBox" id="recommended_listbox">
+            <property name="visible">True</property>
+            <property name="vexpand">True</property>
+            <property name="selection-mode">none</property>
+            <signal name="row-activated" handler="on_listbox_row_activated"/>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkButton" id="show_more_button">
+            <property name="visible">True</property>
+            <signal name="clicked" handler="on_show_more_button_clicked"/>
+            <style>
+              <class name="list-expand-button"/>
+              <class name="boxes-menu-row"/>
+              <class name="flat"/>
+            </style>
+
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="icon-name">view-more-symbolic</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="name">recommended</property>
+      </packing>
+    </child>
+
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">True</property>
+        <property name="expand">True</property>
+
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <property name="border-width">20</property>
+            <property name="margin-start">20</property>
+            <property name="margin-end">20</property>
+
+            <child>
+              <object class="GtkListBox" id="listbox">
+                <property name="visible">True</property>
+                <property name="selection-mode">none</property>
+                <signal name="row-activated" handler="on_listbox_row_activated"/>
+                <style>
+                  <class name="sources-list"/>
+                  <class name="frame"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="name">search-results</property>
+      </packing>
+    </child>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="opacity">0.5</property>
+        <property name="spacing">10</property>
+        <property name="valign">center</property>
+        <style>
+          <class name="dim-label"/>
+        </style>
+
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="resource">/org/gnome/Boxes/icons/empty-boxes.png</property>
+            <property name="pixel-size">128</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="margin">18</property>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">No operating systems found</property>
+            <attributes>
+              <attribute name="scale" value="2"/>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Try a different search</property>
+          </object>
+        </child>
+
+      </object>
+      <packing>
+        <property name="name">no-results</property>
+      </packing>
+    </child>
+
+  </template>
+
+       <object class="GtkSearchEntry" id="searchbar">
+            <property name="visible">True</property>
+            <property name="width-chars">50</property>
+            <property name="can-focus">True</property>
+            <property name="placeholder-text" translatable="yes">Search for an OS or enter a download 
link…</property>
+          </object>
+</interface>
diff --git a/data/ui/assistant/pages/index-page.ui b/data/ui/assistant/pages/index-page.ui
new file mode 100644
index 00000000..6eb98db7
--- /dev/null
+++ b/data/ui/assistant/pages/index-page.ui
@@ -0,0 +1,305 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantIndexPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Create a Virtual Machine</property>
+
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="visible">True</property>
+        <signal name="notify::visible-child" handler="update_topbar"/>
+
+        <child>
+          <object class="GtkScrolledWindow" id="home_page">
+            <property name="visible">True</property>
+            <property name="expand">True</property>
+            <property name="hscrollbar-policy">never</property>
+
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">30</property>
+                <property name="halign">center</property>
+                <property name="border-width">20</property>
+                <property name="margin-left">30</property>
+                <property name="margin-right">30</property>
+
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="wrap">True</property>
+                    <property name="max-width-chars">60</property>
+                    <property name="xalign">0</property>
+                    <property name="halign">start</property>
+                    <property name="label" translatable="yes">A new virtual machine will be created and an 
operating system installed into it. Select an operating system source to begin.</property>
+                  </object>
+                </child>
+
+                <child>
+                  <object class="GtkBox" id="detected_sources_section">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <property name="orientation">vertical</property>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Detected Sources</property>
+                        <style>
+                          <class name="bold-label"/>
+                        </style>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="orientation">vertical</property>
+                        <style>
+                          <class name="linked"/>
+                          <class name="frame"/>
+                          <class name="sources-list"/>
+                        </style>
+
+                        <child>
+                          <object class="GtkListBox" id="source_medias">
+                            <property name="visible">True</property>
+                            <signal name="row-activated" handler="on_source_media_selected"/>
+                            <style>
+                              <class name="sources-list"/>
+                            </style>
+                          </object>
+                        </child>
+
+                        <child>
+                          <object class="GtkButton" id="expand_detected_sources_list_button">
+                            <property name="visible">True</property>
+                            <signal name="clicked" handler="on_expand_detected_sources_list"/>
+                            <style>
+                              <class name="flat"/>
+                              <class name="list-expand-button"/>
+                              <class name="boxes-menu-row"/>
+                            </style>
+
+                            <child>
+                              <object class="GtkImage">
+                                <property name="visible">True</property>
+                                <property name="icon-name">view-more-symbolic</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <property name="orientation">vertical</property>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Featured Downloads</property>
+                        <style>
+                          <class name="bold-label"/>
+                        </style>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">You will be notified when the download has 
completed.</property>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkListBox" id="featured_medias">
+                        <property name="visible">True</property>
+                        <signal name="row-activated" handler="on_featured_media_selected"/>
+                        <style>
+                          <class name="frame"/>
+                          <class name="sources-list"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                  </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <property name="orientation">vertical</property>
+
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="xalign">0</property>
+                        <property name="label" translatable="yes">Select an OS Source</property>
+                        <style>
+                          <class name="bold-label"/>
+                        </style>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="orientation">vertical</property>
+                        <style>
+                          <class name="linked"/>
+                        </style>
+
+                        <child>
+                          <object class="GtkButton">
+                            <property name="visible">True</property>
+                            <signal name="clicked" handler="on_download_an_os_button_clicked"/>
+                            <style>
+                              <class name="boxes-menu-row"/>
+                            </style>
+
+                            <child>
+                              <object class="GtkGrid">
+                                <property name="visible">True</property>
+                                <property name="border-width">10</property>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Operating System 
Download</property>
+                                    <style>
+                                      <class name="bold-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">0</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Browse and search for 
operating systems to install.</property>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">1</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="icon-name">go-next-symbolic</property>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">1</property>
+                                    <property name="top-attach">0</property>
+                                    <property name="height">2</property>
+                                  </packing>
+                                </child>
+                              </object>
+                            </child>
+
+                          </object>
+                        </child>
+
+                        <child>
+                          <object class="GtkButton">
+                            <property name="visible">True</property>
+                            <signal name="clicked" handler="on_select_file_button_clicked"/>
+                            <style>
+                              <class name="boxes-menu-row"/>
+                            </style>
+
+                            <child>
+                              <object class="GtkGrid">
+                                <property name="visible">True</property>
+                                <property name="border-width">10</property>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Operating System Image 
File</property>
+                                    <style>
+                                      <class name="bold-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">0</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkLabel">
+                                    <property name="visible">True</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="halign">start</property>
+                                    <property name="label" translatable="yes">Select an .iso file to install 
a virtual machine.</property>
+                                    <style>
+                                      <class name="dim-label"/>
+                                    </style>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">0</property>
+                                    <property name="top-attach">1</property>
+                                  </packing>
+                                </child>
+
+                                <child>
+                                  <object class="GtkImage">
+                                    <property name="visible">True</property>
+                                    <property name="icon-name">go-next-symbolic</property>
+                                  </object>
+                                  <packing>
+                                    <property name="left-attach">1</property>
+                                    <property name="top-attach">0</property>
+                                    <property name="height">2</property>
+                                  </packing>
+                                </child>
+                              </object>
+                            </child>
+
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+
+        <child>
+          <object class="BoxesAssistantDownloadsPage" id="recommended_downloads_page">
+            <property name="visible">True</property>
+            <signal name="media-selected" handler="on_featured_media_selected"/>
+          </object>
+        </child>
+
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/preparation-page.ui b/data/ui/assistant/pages/preparation-page.ui
new file mode 100644
index 00000000..9ddec630
--- /dev/null
+++ b/data/ui/assistant/pages/preparation-page.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantPreparationPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Preparing…</property>
+
+    <child>
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="expand">True</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="column-spacing">10</property>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="wrap">True</property>
+            <property name="halign">start</property>
+            <property name="label" translatable="yes">Preparing to create a new box</property>
+            <property name="margin-bottom">20</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">0</property>
+            <property name="width">2</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkImage" id="installer_image">
+            <property name="visible">True</property>
+            <property name="icon-size">0</property>
+            <property name="pixel-size">128</property>
+            <property name="icon-name">media-optical</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">1</property>
+            <property name="height">3</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkLabel" id="media_label">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="valign">end</property>
+            <style>
+              <class name="boxes-wizard-media-os-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">1</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkLabel" id="status_label">
+            <property name="visible">True</property>
+            <property name="halign">start</property>
+            <property name="valign">center</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">2</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkProgressBar" id="progress_bar">
+            <property name="visible">True</property>
+            <property name="valign">start</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">3</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/review-page.ui b/data/ui/assistant/pages/review-page.ui
new file mode 100644
index 00000000..a135151d
--- /dev/null
+++ b/data/ui/assistant/pages/review-page.ui
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantReviewPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Review and Create</property>
+    <property name="vexpand">False</property>
+    <property name="valign">start</property>
+    <property name="orientation">vertical</property>
+    <property name="border-width">30</property>
+    <property name="spacing">20</property>
+
+    <child>
+      <object class="GtkLabel" id="review_label">
+        <property name="visible">True</property>
+        <property name="halign">start</property>
+        <property name="wrap">True</property>
+        <property name="width-chars">30</property>
+        <property name="label" translatable="yes">Boxes is ready to set up a new box with the following 
properties:</property>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkInfoBar" id="nokvm_infobar">
+        <property name="visible">True</property>
+        <property name="halign">fill</property>
+        <property name="spacing">0</property>
+        <property name="message-type">warning</property>
+
+        <child internal-child="content_area">
+          <object class="GtkContainer" id="nokvm_container">
+            <property name="visible">True</property>
+
+            <child>
+              <object class="GtkImage" id="nokvm_image">
+                <property name="visible">True</property>
+                <property name="icon-name">dialog-warning</property>
+                <property name="icon-size">3</property>
+                <property name="pixel-size">48</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkLabel" id="nokvm_label">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Virtualization extensions are unavailable on your 
system.
+Check your BIOS settings to enable them.</property>
+                <property name="wrap">True</property>
+                <property name="halign">start</property>
+                <property name="hexpand">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+      </packing>
+    </child>
+
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Resource Allocation</property>
+            <style>
+              <class name="bold-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton" id="customize_button">
+            <property name="visible">True</property>
+            <property name="label" translatable="yes">Customize</property>
+            <property name="halign">end</property>
+            <property name="hexpand">True</property>
+            <signal name="toggled" handler="on_customize_button_toggled"/>
+          </object>
+        </child>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkStack" id="customization_stack">
+        <property name="visible">True</property>
+        <child>
+          <object class="BoxesWizardSummary" id="summary"/>
+        </child>
+        <child>
+          <object class="GtkGrid" id="customization_grid">
+            <property name="visible">True</property>
+            <property name="row_spacing">10</property>
+            <property name="column_spacing">20</property>
+          </object>
+        </child>
+      </object>
+
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">False</property>
+      </packing>
+    </child>
+
+  </template>
+</interface>
diff --git a/data/ui/assistant/pages/setup-page.ui b/data/ui/assistant/pages/setup-page.ui
new file mode 100644
index 00000000..f5425b7a
--- /dev/null
+++ b/data/ui/assistant/pages/setup-page.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesAssistantSetupPage" parent="BoxesAssistantPage">
+    <property name="visible">True</property>
+    <property name="title" translatable="yes">Express Installation</property>
+    <property name="expand">True</property>
+
+    <child>
+      <object class="GtkBox" id="setup_box">
+        <property name="visible">True</property>
+        <property name="expand">True</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">0</property>
+        <property name="margin-start">10</property>
+        <property name="margin-end">10</property>
+        <property name="valign">center</property>
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/data/ui/assistant/rhel-download-dialog.ui b/data/ui/assistant/rhel-download-dialog.ui
new file mode 100644
index 00000000..ad8b9e1c
--- /dev/null
+++ b/data/ui/assistant/rhel-download-dialog.ui
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesRHELDownloadDialog" parent="GtkDialog">
+    <property name="modal">True</property>
+    <property name="type-hint">dialog</property>
+    <property name="height-request">250</property>
+
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+
+        <child>
+          <object class="GtkOverlay" id="overlay">
+            <property name="visible">True</property>
+
+            <child type="overlay">
+              <object class="GtkProgressBar" id="progress_bar">
+                <property name="no-show-all">True</property>
+                <property name="valign">start</property>
+                <style>
+                  <class name="osd"/>
+                </style>
+              </object>
+            </child>
+
+            <child>
+              <!-- https://bugzilla.gnome.org/show_bug.cgi?id=786932 -->
+              <!-- https://bugzilla.gnome.org/show_bug.cgi?id=787033 -->
+              <!-- https://bugs.webkit.org/show_bug.cgi?id=175937 -->
+              <object class="WebKitWebView" type-func="webkit_web_view_get_type" id="web_view">
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="visible">True</property>
+                <signal name="context-menu" handler="on_context_menu" />
+                <signal name="notify::estimated-load-progress" handler="on_notify_estimated_load_progress" />
+                <signal name="decide-policy" handler="on_decide_policy"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/assistant/vm-assistant.ui b/data/ui/assistant/vm-assistant.ui
new file mode 100644
index 00000000..71744f21
--- /dev/null
+++ b/data/ui/assistant/vm-assistant.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="BoxesVMAssistant" parent="GtkDialog">
+    <property name="modal">True</property>
+    <property name="type-hint">dialog</property>
+    <property name="title" translatable="yes">Create a Virtual Machine</property>
+    <property name="width-request">724</property>
+    <property name="height-request">568</property>
+
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+
+        <child>
+          <object class="GtkStack" id="pages">
+            <property name="visible">True</property>
+            <signal name="notify::visible-child" handler="update_titlebar"/>
+            <child>
+              <object class="BoxesAssistantIndexPage" id="index_page">
+                <signal name="done" handler="do_preparation"/>
+              </object>
+            </child>
+            <child>
+              <object class="BoxesAssistantPreparationPage" id="preparation_page">
+                <signal name="done" handler="do_setup"/>
+              </object>
+            </child>
+            <child>
+              <object class="BoxesAssistantSetupPage" id="setup_page">
+                <signal name="done" handler="do_review"/>
+              </object>
+            </child>
+            <child>
+              <object class="BoxesAssistantReviewPage" id="review_page">
+                <signal name="done" handler="do_create"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+
+    <child type="action">
+      <object class="GtkButton" id="previous_button">
+        <property name="visible">True</property>
+        <property name="label" translatable="yes">Cancel</property>
+        <signal name="clicked" handler="on_previous_button_clicked"/>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="next_button">
+        <property name="visible">False</property>
+        <property name="label" translatable="yes">Next</property>
+        <signal name="clicked" handler="on_next_button_clicked"/>
+      </object>
+    </child>
+
+    <action-widgets>
+      <action-widget response="cancel">previous_button</action-widget>
+      <action-widget response="ok">next_button</action-widget>
+    </action-widgets>
+
+  </template>
+</interface>
diff --git a/data/ui/collection-toolbar.ui b/data/ui/collection-toolbar.ui
index a9810b6e..5a13191d 100644
--- a/data/ui/collection-toolbar.ui
+++ b/data/ui/collection-toolbar.ui
@@ -61,6 +61,33 @@
       </object>
     </child>
 
+    <child>
+      <object class="GtkMenuButton" id="downloads_hub_btn">
+        <property name="visible">False</property>
+        <property name="valign">center</property>
+        <property name="use-underline">True</property>
+        <style>
+          <class name="image-button"/>
+        </style>
+        <child internal-child="accessible">
+          <object class="AtkObject">
+            <property name="accessible-name" translatable="yes">Downloads</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage">
+            <property name="visible">True</property>
+            <property name="icon-name">media-record-symbolic</property>
+            <property name="icon-size">1</property>
+          </object>
+        </child>
+      </object>
+
+      <packing>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+
     <child>
       <object class="GtkMenuButton" id="hamburger_btn">
         <property name="visible">True</property>
@@ -210,7 +237,7 @@
           <object class="GtkModelButton">
             <property name="visible">True</property>
             <property name="text" translatable="yes">Create a Virtual Machine…</property>
-            <signal name="clicked" handler="on_new_vm_btn_clicked"/>
+            <signal name="clicked" handler="on_create_vm_btn_clicked"/>
           </object>
         </child>
         <child>
diff --git a/data/ui/downloads-hub-row.ui b/data/ui/downloads-hub-row.ui
new file mode 100644
index 00000000..daf6a026
--- /dev/null
+++ b/data/ui/downloads-hub-row.ui
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.19"/>
+  <template class="BoxesDownloadsHubRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <property name="selectable">False</property>
+
+    <child>
+      <object class="GtkGrid">
+        <property name="visible">True</property>
+        <property name="border-width">10</property>
+        <property name="column-spacing">20</property>
+
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="visible">True</property>
+            <property name="icon-name">media-optical</property>
+            <property name="icon-size">0</property>
+            <property name="pixel-size">64</property>
+          </object>
+          <packing>
+            <property name="height">2</property>
+            <property name="left-attach">0</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="visible">True</property>
+            <property name="wrap">True</property>
+            <property name="max-width-chars">40</property>
+            <property name="halign">start</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkProgressBar" id="progress_bar">
+            <property name="visible">True</property>
+            <property name="hexpand">True</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkButton">
+            <property name="visible">True</property>
+            <property name="valign">center</property>
+            <signal name="clicked" handler="cancel_download"/>
+
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="icon-name">window-close-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left-attach">2</property>
+            <property name="height">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/downloads-hub.ui b/data/ui/downloads-hub.ui
new file mode 100644
index 00000000..5d0665c2
--- /dev/null
+++ b/data/ui/downloads-hub.ui
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.19"/>
+  <template class="BoxesDownloadsHub" parent="GtkPopover">
+    <property name="can_focus">False</property>
+    <property name="modal">True</property>
+    <property name="position">bottom</property>
+
+    <child>
+      <object class="GtkListBox" id="listbox">
+        <property name="visible">True</property>
+        <signal name="row-activated" handler="on_row_activated"/>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/wizard-downloadable-entry.ui b/data/ui/wizard-downloadable-entry.ui
index 10e4bb9c..480244e3 100644
--- a/data/ui/wizard-downloadable-entry.ui
+++ b/data/ui/wizard-downloadable-entry.ui
@@ -3,9 +3,6 @@
   <!-- interface-requires gtk+ 3.9 -->
   <template class="BoxesWizardDownloadableEntry" parent="GtkListBoxRow">
     <property name="visible">True</property>
-    <style>
-      <class name="boxes-menu-row"/>
-    </style>
 
     <child>
       <object class="GtkBox">
diff --git a/data/ui/wizard-media-entry.ui b/data/ui/wizard-media-entry.ui
index 401470cd..050e3415 100644
--- a/data/ui/wizard-media-entry.ui
+++ b/data/ui/wizard-media-entry.ui
@@ -2,10 +2,11 @@
 <interface>
   <!-- interface-requires gtk+ 3.9 -->
   <template class="BoxesWizardMediaEntry" parent="GtkListBoxRow">
-    <property name="visible">True</property>
     <style>
-      <class name="boxes-menu-row"/>
+      <class name="entry-row"/>
     </style>
+
+    <property name="visible">True</property>
     <child>
       <object class="GtkBox" id="hbox">
         <property name="visible">True</property>
diff --git a/src/app-window.vala b/src/app-window.vala
index 82a9f834..46d20ed4 100644
--- a/src/app-window.vala
+++ b/src/app-window.vala
@@ -232,7 +232,7 @@ private void ui_state_changed () {
                                             icon_view,
                                             list_view,
                                             props_window,
-                                            wizard_window,
+                                            //wizard_window,
                                             empty_boxes }) {
             ui.set_state (ui_state);
         }
@@ -303,6 +303,10 @@ public void show_remote_connection_assistant () {
         new Boxes.RemoteConnectionAssistant (this).run ();
     }
 
+    public void show_vm_assistant (string? path = null) {
+        new Boxes.VMAssistant (this, path).run ();
+    }
+
     public void show_properties () {
         if (current_item != null) {
             if (ui_state == UIState.COLLECTION && selection_mode)
diff --git a/src/app.vala b/src/app.vala
index c77b5e04..dd6509d8 100644
--- a/src/app.vala
+++ b/src/app.vala
@@ -91,6 +91,10 @@ public App () {
         action.activate.connect ((param) => { open_name (param.get_string ()); });
         add_action (action);
 
+        action = new GLib.SimpleAction ("install", GLib.VariantType.STRING);
+        action.activate.connect ((param) => { install (param.get_string ()); });
+        add_action (action);
+
         action = new GLib.SimpleAction ("about", null);
         action.activate.connect (() => {
             string[] authors = {
@@ -313,6 +317,10 @@ public void open_name (string name) {
         }
     }
 
+    public void install (string path) {
+        main_window.show_vm_assistant (path);
+    }
+
     public bool open_uuid (string uuid) {
         main_window.set_state (UIState.COLLECTION);
 
@@ -450,7 +458,7 @@ private async void setup_default_source () ensures (default_connection != null)
         }
     }
 
-    private new void send_notification (string notification_id, GLib.Notification notification) {
+    public new void send_notification (string notification_id, GLib.Notification notification) {
         base.send_notification (notification_id, notification);
 
         system_notifications.append (notification_id);
diff --git a/src/assistant/assistant-page.vala b/src/assistant/assistant-page.vala
new file mode 100644
index 00000000..589500db
--- /dev/null
+++ b/src/assistant/assistant-page.vala
@@ -0,0 +1,17 @@
+using Gtk;
+
+private abstract class Boxes.AssistantPage : Gtk.Box {
+    protected Object? artifact;
+    public bool skip = false;
+    protected signal void done (Object artifact);
+
+    public virtual string title {
+        protected set; get;
+    }
+
+    public async virtual void next () {
+        done (artifact);
+    }
+
+    public abstract void cleanup ();
+}
diff --git a/src/assistant/downloads-page.vala b/src/assistant/downloads-page.vala
new file mode 100644
index 00000000..41b93581
--- /dev/null
+++ b/src/assistant/downloads-page.vala
@@ -0,0 +1,118 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+public enum AssistantDownloadsPageView {
+    RECOMMENDED,
+    SEARCH_RESULTS,
+    NO_RESULTS,
+}
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/downloads-page.ui")]
+public class Boxes.AssistantDownloadsPage : Gtk.Stack {
+    private OSDatabase os_db = new OSDatabase ();
+    public DownloadsSearch search { private set; get; }
+
+    [GtkChild]
+    private Gtk.ListBox listbox;
+    [GtkChild]
+    private Gtk.ListBox recommended_listbox;
+
+    public Gtk.SearchEntry search_entry = new Gtk.SearchEntry ();
+    private GLib.ListStore recommended_model;
+
+    public signal void media_selected (Gtk.ListBoxRow row);
+
+    private AssistantDownloadsPageView _page;
+    public AssistantDownloadsPageView page {
+        get { return _page; }
+        set {
+            _page = value;
+
+            switch (_page) {
+                case AssistantDownloadsPageView.SEARCH_RESULTS:
+                    visible_child_name = "search-results";
+                    break;
+                case AssistantDownloadsPageView.NO_RESULTS:
+                    visible_child_name = "no-results";
+                    break;
+                case AssistantDownloadsPageView.RECOMMENDED:
+                default:
+                    visible_child_name = "recommended";
+                    break;
+            }
+        }
+    }
+
+    construct {
+        os_db.load.begin ();
+
+        search = new DownloadsSearch ();
+
+        // TODO: move this into a UI file
+        search_entry.search_changed.connect (on_search_changed);
+        search_entry.width_chars = 50;
+        search_entry.can_focus = true;
+        search_entry.placeholder_text = _("Search for an OS or enter a download link…");
+        search_entry.visible = true;
+
+        recommended_model = new GLib.ListStore (typeof (Osinfo.Media));
+        recommended_listbox.bind_model (recommended_model, create_downloads_entry);
+        recommended_listbox.set_header_func (use_list_box_separator);
+        populate_recommended_list.begin ();
+
+        listbox.bind_model (search.model, create_downloads_entry);
+        listbox.set_header_func (use_list_box_separator);
+
+        search.search_changed.connect (set_visible_view);
+    }
+
+    private void set_visible_view () {
+        if (search.text.length == 0) {
+            page = AssistantDownloadsPageView.RECOMMENDED;
+        } else if (search.model.get_n_items () == 0) {
+            page = AssistantDownloadsPageView.NO_RESULTS;
+        } else {
+            page = AssistantDownloadsPageView.SEARCH_RESULTS;
+        }
+    }
+
+    private async void populate_recommended_list () {
+        foreach (var media in yield get_recommended_downloads ()) {
+            if (media != null) {
+                recommended_model.append (media);
+            }
+        }
+    }
+
+    private Gtk.Widget create_downloads_entry (Object item) {
+        return new WizardDownloadableEntry (item as Osinfo.Media);
+    }
+
+    [GtkCallback]
+    private void on_listbox_row_activated (Gtk.ListBoxRow row) {
+        media_selected (row);
+    }
+
+    [GtkCallback]
+    private void on_show_more_button_clicked () {
+        search.show_all ();
+
+        page = AssistantDownloadsPageView.SEARCH_RESULTS;
+    }
+
+    private void on_search_changed () {
+        var text = search_entry.get_text ();
+
+        if (text == null)
+            return;
+
+        search.text = text;
+    }
+
+    [GtkCallback]
+    private bool on_key_pressed (Gtk.Widget widget, Gdk.EventKey event) {
+        if (!search_entry.has_focus)
+            search_entry.grab_focus ();
+
+        return search_entry.key_press_event (event);
+    }
+}
diff --git a/src/assistant/index-page.vala b/src/assistant/index-page.vala
new file mode 100644
index 00000000..8587dd48
--- /dev/null
+++ b/src/assistant/index-page.vala
@@ -0,0 +1,159 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/index-page.ui")]
+private class Boxes.AssistantIndexPage : AssistantPage {
+    GLib.ListStore source_model = new GLib.ListStore (typeof (InstallerMedia));
+    GLib.ListStore featured_model = new GLib.ListStore (typeof (Osinfo.Media));
+
+    private VMAssistant dialog;
+
+    private GLib.List<InstallerMedia> installer_medias;
+
+    private const int MAX_MEDIA_ENTRIES = 3;
+
+    [GtkChild]
+    private Stack stack;
+    [GtkChild]
+    private AssistantDownloadsPage recommended_downloads_page;
+    [GtkChild]
+    private ScrolledWindow home_page;
+
+    [GtkChild]
+    private Box detected_sources_section;
+    [GtkChild]
+    private ListBox source_medias;
+    [GtkChild]
+    private ListBox featured_medias;
+    [GtkChild]
+    private Button expand_detected_sources_list_button;
+
+    private GLib.Cancellable cancellable = new GLib.Cancellable ();
+
+    construct {
+        populate_media_lists.begin ();
+
+        source_medias.bind_model (source_model, add_media_entry);
+        featured_medias.bind_model (featured_model, add_featured_media_entry);
+
+        source_medias.set_header_func (use_list_box_separator);
+        featured_medias.set_header_func (use_list_box_separator);
+    }
+
+    public void setup (VMAssistant dialog) {
+        this.dialog = dialog;
+    }
+
+    public void go_back () {
+        if (stack.visible_child == home_page) {
+            dialog.shutdown ();
+
+            return;
+        }
+
+        stack.visible_child = home_page;
+        update_topbar ();
+    }
+
+    private async void populate_media_lists () {
+        var media_manager = MediaManager.get_instance ();
+
+        installer_medias = yield media_manager.list_installer_medias ();
+        populate_detected_sources_list (MAX_MEDIA_ENTRIES);
+
+        var recommended_downloads = yield get_recommended_downloads ();
+        for (var i = 0; i < MAX_MEDIA_ENTRIES; i++)
+            featured_model.append (recommended_downloads.nth (i).data);
+    }
+
+    private void populate_detected_sources_list (int? number_of_items = null) {
+       var number_of_available_medias = installer_medias.length ();
+        detected_sources_section.visible = (number_of_available_medias > 0);
+        source_model.remove_all ();
+
+        if (number_of_available_medias == 0)
+            return;
+
+       expand_detected_sources_list_button.visible = (number_of_available_medias > MAX_MEDIA_ENTRIES);
+
+        foreach (var media in installer_medias) {
+            source_model.append (media);
+
+            if (number_of_items != null && ((number_of_items -= 1) == 0))
+                return;
+        }
+    }
+
+    private Gtk.Widget add_media_entry (GLib.Object object) {
+        return new WizardMediaEntry (object as InstallerMedia);
+    }
+
+    private Gtk.Widget add_featured_media_entry (GLib.Object object) {
+        return new WizardDownloadableEntry (object as Osinfo.Media);
+    }
+
+    [GtkCallback]
+    private void update_topbar () {
+        dialog.previous_button.label = _("Cancel");
+
+        var titlebar = dialog.get_titlebar () as Gtk.HeaderBar;
+        if (stack.visible_child == recommended_downloads_page) {
+            titlebar.set_custom_title (recommended_downloads_page.search_entry);
+        } else {
+            titlebar.set_custom_title (null);
+        }
+    }
+
+    [GtkCallback]
+    private void on_expand_detected_sources_list () {
+        populate_detected_sources_list ();
+
+        expand_detected_sources_list_button.hide ();
+    }
+
+    [GtkCallback]
+    private void on_source_media_selected (Gtk.ListBoxRow row) {
+        var entry = row as WizardMediaEntry;
+
+        if (entry.media != null)
+            done (entry.media);
+    }
+
+    [GtkCallback]
+    private void on_featured_media_selected (Gtk.ListBoxRow row) {
+        var entry = row as WizardDownloadableEntry;
+
+        if (entry.os != null && entry.os.id.has_prefix ("http://redhat.com/rhel/";)) {
+            (new RHELDownloadDialog (dialog, entry).run ());
+        } else {
+            DownloadsHub.get_instance ().add_item (entry);
+        }
+
+        dialog.shutdown ();
+    }
+
+    public override void cleanup () {
+        cancellable.cancel ();
+    }
+
+    [GtkCallback]
+    private async void on_select_file_button_clicked () {
+        var file_chooser = new Gtk.FileChooserNative (_("Select a device or ISO file"),
+                                                      App.app.main_window,
+                                                      Gtk.FileChooserAction.OPEN,
+                                                      _("Open"), _("Cancel"));
+        file_chooser.bind_property ("visible", dialog, "visible", BindingFlags.INVERT_BOOLEAN);
+        if (file_chooser.run () == Gtk.ResponseType.ACCEPT) {
+            var media_manager = MediaManager.get_instance ();
+            var media = yield media_manager.create_installer_media_for_path (file_chooser.get_filename (),
+                                                                             cancellable);
+            done (media);
+        }
+    }
+
+    [GtkCallback]
+    private void on_download_an_os_button_clicked () {
+        stack.set_visible_child (recommended_downloads_page);
+
+        dialog.previous_button.label = _("Previous");
+    }
+}
diff --git a/src/assistant/preparation-page.vala b/src/assistant/preparation-page.vala
new file mode 100644
index 00000000..aa6d426d
--- /dev/null
+++ b/src/assistant/preparation-page.vala
@@ -0,0 +1,69 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/preparation-page.ui")]
+private class Boxes.AssistantPreparationPage : AssistantPage {
+    [GtkChild]
+    private Gtk.Label media_label;
+    [GtkChild]
+    private Gtk.Label status_label;
+    [GtkChild]
+    private Gtk.Image installer_image;
+    [GtkChild]
+    private Gtk.ProgressBar progress_bar;
+
+    private Cancellable cancellable = new GLib.Cancellable ();
+
+    private InstallerMedia _media;
+    public InstallerMedia media {
+        get { return _media; }
+        set {
+            _media = value;
+
+            if (_media.os != null) {
+                media_label.label = _media.os.name;
+                Downloader.fetch_os_logo.begin (installer_image, _media.os, 128);
+            }
+        }
+    }
+
+    public void setup (InstallerMedia media) {
+        try {
+            var media_manager = MediaManager.get_instance ();
+            media = media_manager.create_installer_media_from_media (media);
+        } catch (GLib.Error error) {
+            warning ("Failed to setup installation media '%s': %s", media.device_file, error.message);
+        }
+
+        prepare (media);
+
+        skip = true;
+    }
+
+    public async void prepare (InstallerMedia media) {
+        var progress = create_preparation_progress ();
+        if (!yield media.prepare (progress, cancellable)) // add cancellable
+            return;
+
+        progress_bar.fraction = 1.0;
+
+        done (media.get_vm_creator ());
+    }
+
+    private ActivityProgress create_preparation_progress () {
+        var progress = new ActivityProgress ();
+
+        progress.notify["progress"].connect (() => {
+            if (progress.progress - progress_bar.fraction >= 0.01)
+                progress_bar.fraction = progress.progress;
+        });
+        progress_bar.fraction = progress.progress = 0;
+
+        progress.bind_property ("info", status_label, "label");
+
+        return progress;
+    }
+
+    public override void cleanup () {
+        cancellable.reset ();
+    }
+}
diff --git a/src/assistant/review-page.vala b/src/assistant/review-page.vala
new file mode 100644
index 00000000..afda063e
--- /dev/null
+++ b/src/assistant/review-page.vala
@@ -0,0 +1,123 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/review-page.ui")]
+private class Boxes.AssistantReviewPage : AssistantPage {
+    [GtkChild]
+    private WizardSummary summary;
+    [GtkChild]
+    private InfoBar nokvm_infobar;
+    [GtkChild]
+    private Grid customization_grid;
+    [GtkChild]
+    private ToggleButton customize_button;
+    [GtkChild]
+    private Stack customization_stack;
+
+    private Cancellable cancellable = new GLib.Cancellable ();
+
+    [GtkCallback]
+    private void on_customize_button_toggled () {
+        customization_stack.set_visible_child (customize_button.active ?
+                                               customization_grid : summary);
+    }
+
+    public async void setup (VMCreator vm_creator) {
+        try {
+            artifact = yield vm_creator.create_vm (cancellable);
+        } catch (IOError.CANCELLED cancel_error) { // We did this, so ignore!
+        } catch (GLib.Error error) {
+            warning ("Box setup failed: %s", error.message);
+        }
+
+        yield populate (artifact as LibvirtMachine);
+    }
+
+    public async void populate (LibvirtMachine machine) {
+        var vm_creator = machine.vm_creator;
+        foreach (var property in vm_creator.install_media.get_vm_properties ())
+            summary.add_property (property.first, property.second);
+
+        try {
+            var config = null as GVirConfig.Domain;
+            yield App.app.async_launcher.launch (() => {
+                config = machine.domain.get_config (GVir.DomainXMLFlags.INACTIVE);
+            });
+
+            var memory = format_size (config.memory * Osinfo.KIBIBYTES, FormatSizeFlags.IEC_UNITS);
+            summary.add_property (_("Memory"), memory);
+        } catch (GLib.Error error) {
+            warning ("Failed to get configuration for machine '%s': %s", machine.name, error.message);
+        }
+
+        if (!machine.importing && machine.storage_volume != null) {
+            try {
+                var volume_info = machine.storage_volume.get_info ();
+                var capacity = format_size (volume_info.capacity);
+                summary.add_property (_("Disk"),
+                                      // Translators: This is disk size. E.g "1 GB maximum".
+                                      _("%s maximum").printf (capacity));
+            } catch (GLib.Error error) {
+                warning ("Failed to get information on volume '%s': %s",
+                         machine.storage_volume.get_name (),
+                         error.message);
+            }
+
+            nokvm_infobar.visible = (machine.domain_config.get_virt_type () != 
GVirConfig.DomainVirtType.KVM);
+        }
+
+        populate_customization_grid (machine);
+    }
+
+    private void populate_customization_grid (LibvirtMachine machine) {
+        var resource_properties = new GLib.List<Boxes.Property> ();
+        machine.properties.get_resources_properties (ref resource_properties);
+
+        return_if_fail (resource_properties.length () > 0);
+
+        var current_row = 0;
+        foreach (var property in resource_properties) {
+            if (property.widget == null || property.extra_widget == null) {
+                warn_if_reached ();
+
+                continue;
+            }
+
+            property.widget.hexpand = true;
+            customization_grid.attach (property.widget, 0, current_row, 1, 1);
+
+            property.extra_widget.hexpand = true;
+            customization_grid.attach (property.extra_widget, 0, current_row + 1, 1, 1);
+
+            current_row += 2;
+        }
+        customization_grid.show_all ();
+    }
+
+    public override void cleanup () {
+        cancellable.cancel ();
+
+        summary.clear ();
+        nokvm_infobar.hide ();
+
+        if (artifact != null) {
+            App.app.delete_machine (artifact as Machine);
+        }
+
+        foreach (var child in customization_grid.get_children ())
+            customization_grid.remove (child);
+    }
+
+    public override async void next () {
+        if (artifact == null) {
+            var wait = notify["artifact"].connect (() => {
+                next.callback ();
+            });
+            yield;
+            disconnect (wait);
+        }
+
+        done (artifact);
+
+        cancellable.reset ();
+    }
+}
diff --git a/src/assistant/rhel-download-dialog.vala b/src/assistant/rhel-download-dialog.vala
new file mode 100644
index 00000000..ae41017a
--- /dev/null
+++ b/src/assistant/rhel-download-dialog.vala
@@ -0,0 +1,182 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/rhel-download-dialog.ui")]
+private class Boxes.RHELDownloadDialog : Gtk.Dialog {
+    [GtkChild]
+    private Gtk.ProgressBar progress_bar;
+    [GtkChild]
+    private WebKit.WebView web_view;
+
+    private uint hide_progress_bar_id;
+    private const uint progress_bar_id_timeout = 500;  // 500ms
+
+    private bool is_rhel8 = false;
+
+    private GLib.Cancellable cancellable = new GLib.Cancellable ();
+
+    private WizardDownloadableEntry entry;
+
+    construct {
+        var context = web_view.get_context ();
+        var language_names = GLib.Intl.get_language_names ();
+        context.set_preferred_languages (language_names);
+
+        cancellable.connect (() => {
+            web_view.stop_loading ();
+            web_view.load_uri ("about:blank");
+
+            var data_manager = web_view.get_website_data_manager ();
+            data_manager.clear.begin (WebKit.WebsiteDataTypes.COOKIES, 0, null);
+        });
+    }
+
+    public RHELDownloadDialog (VMAssistant dialog, WizardDownloadableEntry entry) {
+        set_transient_for (App.app.main_window);
+        this.entry = entry;
+
+        var user_agent = GLib.Uri.escape_string (get_user_agent (), null, false);
+        var authentication_uri = "https://developers.redhat.com/download-manager/rest/featured/file/rhel"; +
+                                 "?tag=" + user_agent;
+
+        var os = entry.os;
+        is_rhel8 = os.id.has_prefix ("http://redhat.com/rhel/8";);
+
+        web_view.load_uri (authentication_uri);
+
+        int width, height;
+        dialog.get_size_request (out width, out height);
+        set_size_request (width, height);
+
+        bind_property ("visible", dialog, "visible", BindingFlags.INVERT_BOOLEAN);
+    }
+
+    [GtkCallback]
+    private bool on_decide_policy (WebKit.WebView web_view,
+                                   WebKit.PolicyDecision decision,
+                                   WebKit.PolicyDecisionType decision_type) {
+        if (decision_type != WebKit.PolicyDecisionType.NAVIGATION_ACTION)
+            return false;
+
+        var action = (decision as WebKit.NavigationPolicyDecision).get_navigation_action ();
+        var request = action.get_request ();
+        var request_uri = request.get_uri ();
+
+        if (!request_uri.has_prefix ("https://developers.redhat.com/products/rhel";) &&
+            !request_uri.has_prefix ("https://access.cdn.redhat.com";))
+            return false;
+
+        var soup_request_uri = new Soup.URI (request_uri);
+        var query = soup_request_uri.get_query ();
+        if (query == null)
+            return false;
+
+        var key_value_pairs = Soup.Form.decode (query);
+
+        var download_uri = is_rhel8 ? request_uri : key_value_pairs.lookup ("tcDownloadURL");
+        if (download_uri == null)
+            return false;
+
+        debug ("RHEL ISO download URI: %s", download_uri);
+
+        entry.url = download_uri;
+        DownloadsHub.get_instance ().add_item (entry);
+
+        decision.ignore ();
+        this.close ();
+
+        return true;
+    }
+
+    public override void dispose () {
+        if (hide_progress_bar_id != 0) {
+            GLib.Source.remove (hide_progress_bar_id);
+            hide_progress_bar_id = 0;
+        }
+
+        base.dispose ();
+    }
+
+    [GtkCallback]
+    private bool on_context_menu (WebKit.WebView web_view,
+                                  WebKit.ContextMenu context_menu,
+                                  Gdk.Event event,
+                                  WebKit.HitTestResult hit_test_result) {
+        var items_to_remove = new GLib.List<WebKit.ContextMenuItem> ();
+
+        foreach (var item in context_menu.get_items ()) {
+            var action = item.get_stock_action ();
+            if (action == WebKit.ContextMenuAction.GO_BACK ||
+                action == WebKit.ContextMenuAction.GO_FORWARD ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_AUDIO_TO_DISK ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_IMAGE_TO_DISK ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_LINK_TO_DISK ||
+                action == WebKit.ContextMenuAction.DOWNLOAD_VIDEO_TO_DISK ||
+                action == WebKit.ContextMenuAction.OPEN_AUDIO_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_FRAME_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_IMAGE_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_LINK_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.OPEN_VIDEO_IN_NEW_WINDOW ||
+                action == WebKit.ContextMenuAction.RELOAD ||
+                action == WebKit.ContextMenuAction.STOP) {
+                items_to_remove.prepend (item);
+            }
+        }
+
+        foreach (var item in items_to_remove) {
+            context_menu.remove (item);
+        }
+
+        var separators_to_remove = new GLib.List<WebKit.ContextMenuItem> ();
+        WebKit.ContextMenuAction previous_action = WebKit.ContextMenuAction.NO_ACTION; // same as a separator
+
+        foreach (var item in context_menu.get_items ()) {
+            var action = item.get_stock_action ();
+            if (action == WebKit.ContextMenuAction.NO_ACTION && action == previous_action)
+                separators_to_remove.prepend (item);
+
+            previous_action = action;
+        }
+
+        foreach (var item in separators_to_remove) {
+            context_menu.remove (item);
+        }
+
+        var n_items = context_menu.get_n_items ();
+        return n_items == 0;
+    }
+
+    [GtkCallback]
+    private void on_notify_estimated_load_progress () {
+        if (hide_progress_bar_id != 0) {
+            GLib.Source.remove (hide_progress_bar_id);
+            hide_progress_bar_id = 0;
+        }
+
+        string? uri = web_view.get_uri ();
+        if (uri == null || uri == "about:blank")
+            return;
+
+        var progress = web_view.get_estimated_load_progress ();
+        bool loading = web_view.is_loading;
+
+        if (progress == 1.0 || !loading) {
+            hide_progress_bar_id = GLib.Timeout.add (progress_bar_id_timeout, () => {
+                progress_bar.hide ();
+                hide_progress_bar_id = 0;
+                return GLib.Source.REMOVE;
+            });
+        } else {
+            progress_bar.show ();
+        }
+
+        progress_bar.set_fraction (loading || progress == 1.0 ? progress : 0.0);
+    }
+
+    public override void close () {
+        cancellable.cancel ();
+
+        base.close ();
+        destroy ();
+    }
+}
diff --git a/src/assistant/setup-page.vala b/src/assistant/setup-page.vala
new file mode 100644
index 00000000..64279ebd
--- /dev/null
+++ b/src/assistant/setup-page.vala
@@ -0,0 +1,34 @@
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/pages/setup-page.ui")]
+private class Boxes.AssistantSetupPage : AssistantPage {
+    [GtkChild]
+    private Box setup_box;
+
+    public async void setup (VMCreator vm_creator) {
+        this.artifact = vm_creator;
+
+        vm_creator.install_media.populate_setup_box (setup_box);
+        if (!vm_creator.install_media.need_user_input_for_vm_creation &&
+             vm_creator.install_media.ready_to_create) {
+            done (vm_creator);
+        }
+
+        skip = !vm_creator.install_media.need_user_input_for_vm_creation;
+    }
+
+    public override async void next () {
+        var vm_creator = artifact as VMCreator;
+        if (vm_creator.install_media.ready_to_create) {
+            done (vm_creator);
+        }
+    }
+
+    public override void cleanup () {
+        if (!skip)
+            return;
+
+        foreach (var child in setup_box.get_children ())
+            child.destroy ();
+    }
+}
diff --git a/src/assistant/vm-assistant.vala b/src/assistant/vm-assistant.vala
new file mode 100644
index 00000000..f63faee9
--- /dev/null
+++ b/src/assistant/vm-assistant.vala
@@ -0,0 +1,149 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/assistant/vm-assistant.ui")]
+private class Boxes.VMAssistant : Gtk.Dialog {
+    [GtkChild]
+    private Stack pages;
+    [GtkChild]
+    private AssistantIndexPage index_page;
+    [GtkChild]
+    private AssistantPreparationPage preparation_page;
+    [GtkChild]
+    private AssistantSetupPage setup_page;
+    [GtkChild]
+    private AssistantReviewPage review_page;
+
+    [GtkChild]
+    public Button previous_button;
+    [GtkChild]
+    private Button next_button;
+
+    private AssistantPage visible_page {
+        get {
+            return pages.get_visible_child () as AssistantPage;
+        }
+    }
+
+    private AssistantPage? previous_page {
+        get {
+            var current_page_index = pages.get_children ().index (visible_page);
+            return pages.get_children ().nth_data (current_page_index - 1) as AssistantPage;
+        }
+    }
+
+    construct {
+        use_header_bar = 1;
+    }
+
+    public VMAssistant (AppWindow app_window, string? path = null) {
+        set_transient_for (app_window);
+
+        // TODO: Make the Assistant independent from window states
+        app_window.set_state (UIState.WIZARD);
+
+        index_page.setup (this);
+
+        if (path != null)
+            prepare_for_path.begin (path);
+    }
+
+    private async void prepare_for_path (string path) {
+        var media_manager = MediaManager.get_instance ();
+
+        try {
+            var installer_media = yield media_manager.create_installer_media_for_path (path, null);
+            do_preparation (installer_media);
+        } catch (GLib.Error error) {
+            debug("Failed to analyze installer image: %s", error.message);
+
+            var msg = _("Failed to analyze installer media. Corrupted or incomplete media?");
+            App.app.main_window.notificationbar.display_error (msg);
+        }
+    }
+
+    [GtkCallback]
+    private void update_titlebar () {
+        var is_index = (visible_page == index_page);
+        var is_last = (visible_page == review_page);
+
+        next_button.visible = !is_index;
+
+        next_button.label = is_last ? _("Create") : _("Next");
+        previous_button.label = is_index ? _("Cancel") : _("Previous");
+
+        title = visible_page.title;
+    }
+
+    [GtkCallback]
+    private void on_previous_button_clicked () {
+        if (visible_page == index_page)
+            index_page.go_back ();
+        else
+            go_back ();
+    }
+
+    private void go_back () {
+        visible_page.cleanup ();
+
+        pages.set_visible_child (previous_page);
+        if (visible_page.skip)
+            go_back ();
+    }
+
+    [GtkCallback]
+    private void on_next_button_clicked () {
+        visible_page.next ();
+    }
+
+    [GtkCallback]
+    private void do_preparation (Object object) {
+        pages.set_visible_child (preparation_page);
+
+        preparation_page.setup (object as InstallerMedia);
+    }
+
+    [GtkCallback]
+    private void do_setup (Object object) {
+        pages.set_visible_child (setup_page);
+
+        var vm_creator = object as VMCreator;
+        vm_creator.install_media.bind_property ("ready-to-create",
+                                                next_button, "sensitive",
+                                                BindingFlags.SYNC_CREATE);
+
+        setup_page.setup (vm_creator);
+    }
+
+    [GtkCallback]
+    private async void do_review (Object object) {
+        pages.set_visible_child (review_page);
+
+        review_page.setup (object as VMCreator);
+    }
+
+    [GtkCallback]
+    private async void do_create (Object object) {
+        var machine = object as LibvirtMachine;
+
+        var vm_creator = machine.vm_creator;
+        try {
+            vm_creator.launch_vm (machine);
+        } catch (GLib.Error error) {
+            warning ("Failed to create machine: %s", error.message);
+
+            // TODO: launch Notification
+        }
+
+        vm_creator.install_media.clean_up_preparation_cache ();
+
+        shutdown ();
+    }
+
+    public void shutdown () {
+        // TODO: Make the Assistant independent from window states
+        App.app.main_window.set_state (UIState.COLLECTION);
+
+        destroy ();
+    }
+}
diff --git a/src/collection-toolbar.vala b/src/collection-toolbar.vala
index 10fd5b94..281058a9 100644
--- a/src/collection-toolbar.vala
+++ b/src/collection-toolbar.vala
@@ -16,6 +16,8 @@
     [GtkChild]
     private Button new_btn;
     [GtkChild]
+    private MenuButton downloads_hub_btn;
+    [GtkChild]
     private MenuButton hamburger_btn;
     [GtkChild]
     private CollectionFilterSwitcher filter_switcher;
@@ -46,6 +48,8 @@ public void setup_ui (AppWindow window) {
         var builder = new Builder.from_resource ("/org/gnome/Boxes/ui/menus.ui");
         MenuModel menu = (MenuModel) builder.get_object ("app-menu");
         hamburger_btn.popover = new Popover.from_model (hamburger_btn, menu);
+
+        downloads_hub_btn.popover = DownloadsHub.get_instance ();
     }
 
     public void click_back_button () {
@@ -61,13 +65,13 @@ public void click_search_button () {
     }
 
     [GtkCallback]
-    private void on_new_vm_btn_clicked () {
-        window.set_state (UIState.WIZARD);
+    private void on_connect_to_remote_btn_clicked () {
+        window.show_remote_connection_assistant ();
     }
 
     [GtkCallback]
-    private void on_connect_to_remote_btn_clicked () {
-        window.show_remote_connection_assistant ();
+    private void on_create_vm_btn_clicked () {
+        window.show_vm_assistant ();
     }
 
     [GtkCallback]
diff --git a/src/downloads-hub.vala b/src/downloads-hub.vala
new file mode 100644
index 00000000..e4740fd1
--- /dev/null
+++ b/src/downloads-hub.vala
@@ -0,0 +1,129 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+using Gtk;
+
+[GtkTemplate (ui = "/org/gnome/Boxes/ui/downloads-hub.ui")]
+private class Boxes.DownloadsHub : Gtk.Popover {
+    private static DownloadsHub instance;
+    public static DownloadsHub get_instance () {
+        if (instance == null)
+            instance = new DownloadsHub ();
+
+        return instance;
+    }
+
+    [GtkChild]
+    private ListBox listbox;
+
+    private bool ongoing_downloads {
+        get { return (listbox.get_children ().length () > 0); }
+    }
+
+    // TODO: inhibit suspend
+
+    public void add_item (WizardDownloadableEntry entry) {
+        var row = new DownloadsHubRow.from_entry (entry);
+
+        if (!relative_to.visible)
+            relative_to.visible = true;
+
+        row.destroy.connect (on_row_deleted);
+        row.download_complete.connect (on_download_complete);
+
+        if (!ongoing_downloads) {
+            var reason = _("Downloading media");
+
+            App.app.inhibit (App.app.main_window, null, reason);
+        }
+
+        listbox.prepend (row);
+    }
+
+    private void on_row_deleted () {
+        if (!ongoing_downloads) {
+            // Hide the Downloads Hub when there aren't ongoing downloads
+            relative_to.visible = false;
+        }
+    }
+
+    private void on_download_complete (string label, string path) {
+        var msg = _("“%s“ download complete").printf (label);
+        var notification = new GLib.Notification (msg);
+        notification.add_button (_("Install"), "app.install::" + path);
+
+        App.app.send_notification ("downloaded-" + label, notification);
+
+        if (!ongoing_downloads) {
+            App.app.uninhibit ();
+        }
+    }
+
+    [GtkCallback]
+    private void on_row_activated (Gtk.ListBoxRow _row) {
+        var row = _row as DownloadsHubRow;
+
+        if (row.local_file != null) {
+            App.app.main_window.show_vm_assistant (row.local_file);
+
+            popup ();
+        }
+    }
+}
+
+[GtkTemplate (ui= "/org/gnome/Boxes/ui/downloads-hub-row.ui")]
+private class Boxes.DownloadsHubRow : Gtk.ListBoxRow {
+    [GtkChild]
+    private Label label;
+    [GtkChild]
+    private Image image;
+    [GtkChild]
+    private ProgressBar progress_bar;
+
+    private ActivityProgress progress = new ActivityProgress ();
+    private ulong progress_notify_id;
+
+    private Cancellable cancellable = new Cancellable ();
+
+    public string? local_file;
+
+    public signal void download_complete (string label, string path);
+
+    public DownloadsHubRow.from_entry (WizardDownloadableEntry entry) {
+        label.label = entry.title;
+
+        Downloader.fetch_os_logo.begin (image, entry.os, 64);
+
+        progress_notify_id = progress.notify["progress"].connect (() => {
+            progress_bar.fraction = progress.progress;
+        });
+        progress_bar.fraction = progress.progress = 0;
+
+        var soup_download_uri = new Soup.URI (entry.url);
+        var download_path = soup_download_uri.get_path ();
+
+        var filename = GLib.Path.get_basename (download_path);
+
+        download.begin (entry.url, filename);
+    }
+
+    private async void download (string url, string filename) {
+        try {
+            local_file = yield Downloader.fetch_media (url, filename, progress, cancellable);
+        } catch (IOError.CANCELLED cancel_error) { // We did this, so ignore!
+        } catch (GLib.Error error) {
+            App.app.main_window.notificationbar.display_error (_("Failed to download"));
+
+            warning (error.message);
+            return;
+        }
+
+        download_complete (label.label, local_file);
+    }
+
+    [GtkCallback]
+    private void cancel_download () {
+        progress.disconnect (progress_notify_id);
+        cancellable.cancel ();
+
+        destroy ();
+    }
+}
diff --git a/src/meson.build b/src/meson.build
index 5b59c3b8..c78b8fbd 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -54,6 +54,7 @@ vala_sources = [
   'display-page.vala',
   'display-toolbar.vala',
   'display.vala',
+  'downloads-hub.vala',
   'editable-entry.vala',
   'i-properties-provider.vala',
   'i-collection-view.vala',
@@ -117,7 +118,15 @@ vala_sources = [
   'troubleshoot-log.vala',
   'snapshot-list-row.vala',
   'snapshots-property.vala',
+  'assistant/rhel-download-dialog.vala',
   'assistant/remote-connection.vala',
+  'assistant/vm-assistant.vala',  
+  'assistant/assistant-page.vala',
+  'assistant/index-page.vala',  
+  'assistant/downloads-page.vala',  
+  'assistant/preparation-page.vala',  
+  'assistant/setup-page.vala',  
+  'assistant/review-page.vala',
 ]
 
 dependencies = [
diff --git a/src/util-app.vala b/src/util-app.vala
index 3dc35cf7..2439e77c 100644
--- a/src/util-app.vala
+++ b/src/util-app.vala
@@ -67,6 +67,22 @@ public void widget_remove (Gtk.Widget widget) {
         container.remove (widget);
     }
 
+    public void use_list_box_separator (ListBoxRow row, ListBoxRow? before_row) {
+        if (before_row == null) {
+            row.set_header (null);
+
+            return;
+        }
+
+        var current = row.get_header ();
+        if (current == null) {
+            current = new Separator (Orientation.HORIZONTAL);
+            current.visible = true;
+
+            row.set_header (current);
+        }
+    }
+
     public Osinfo.Device? find_device_by_prop (Osinfo.DeviceList devices, string prop_name, string 
prop_value) {
         var filter = new Osinfo.Filter ();
         filter.add_constraint (prop_name, prop_value);
diff --git a/src/vm-creator.vala b/src/vm-creator.vala
index 8851bd4b..7fe0d7f6 100644
--- a/src/vm-creator.vala
+++ b/src/vm-creator.vala
@@ -3,7 +3,7 @@
 using Osinfo;
 using GVir;
 
-private class Boxes.VMCreator {
+private class Boxes.VMCreator : Object {
     // Seems installers aren't very consistent about exact number of bytes written so we ought to leave some 
margin
     // of error. It's better to report '100%' done while it's not exactly 100% than reporting '99%' done 
forever..
     private const int INSTALL_COMPLETE_PERCENT = 99;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]