diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc
index a524a17e7..974079e78 100644
--- a/dist/qt_themes/default/default.qrc
+++ b/dist/qt_themes/default/default.qrc
@@ -12,6 +12,18 @@
 
         <file alias="16x16/lock.png">icons/16x16/lock.png</file>
 
+        <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
+      
+        <file alias="48x48/chip.png">icons/48x48/chip.png</file>
+
+        <file alias="48x48/folder.png">icons/48x48/folder.png</file>
+
+        <file alias="48x48/plus.png">icons/48x48/plus.png</file>
+      
+        <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
+      
         <file alias="256x256/citra.png">icons/256x256/citra.png</file>
+
+        <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
     </qresource>
 </RCC>
diff --git a/dist/qt_themes/default/icons/256x256/plus_folder.png b/dist/qt_themes/default/icons/256x256/plus_folder.png
new file mode 100644
index 000000000..5822f34c5
Binary files /dev/null and b/dist/qt_themes/default/icons/256x256/plus_folder.png differ
diff --git a/dist/qt_themes/default/icons/48x48/bad_folder.png b/dist/qt_themes/default/icons/48x48/bad_folder.png
new file mode 100644
index 000000000..32e5bffc2
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/bad_folder.png differ
diff --git a/dist/qt_themes/default/icons/48x48/chip.png b/dist/qt_themes/default/icons/48x48/chip.png
new file mode 100644
index 000000000..d07a85aca
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/chip.png differ
diff --git a/dist/qt_themes/default/icons/48x48/folder.png b/dist/qt_themes/default/icons/48x48/folder.png
new file mode 100644
index 000000000..3fb82f3ae
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/folder.png differ
diff --git a/dist/qt_themes/default/icons/48x48/plus.png b/dist/qt_themes/default/icons/48x48/plus.png
new file mode 100644
index 000000000..dbc74687b
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/plus.png differ
diff --git a/dist/qt_themes/default/icons/48x48/sd_card.png b/dist/qt_themes/default/icons/48x48/sd_card.png
new file mode 100644
index 000000000..3226347e2
Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/default/icons/index.theme b/dist/qt_themes/default/icons/index.theme
index ac67cb236..1edbe6408 100644
--- a/dist/qt_themes/default/icons/index.theme
+++ b/dist/qt_themes/default/icons/index.theme
@@ -1,10 +1,13 @@
 [Icon Theme]
 Name=default
 Comment=default theme
-Directories=16x16,256x256
+Directories=16x16,48x48,256x256
  
 [16x16]
 Size=16
+
+[48x48]
+Size=48
  
 [256x256]
 Size=256
\ No newline at end of file
diff --git a/dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png b/dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png
new file mode 100644
index 000000000..d26c08d6b
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/256x256/plus_folder.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png b/dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png
new file mode 100644
index 000000000..f7f383089
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/bad_folder.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/chip.png b/dist/qt_themes/qdarkstyle/icons/48x48/chip.png
new file mode 100644
index 000000000..f1fa5020a
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/chip.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/folder.png b/dist/qt_themes/qdarkstyle/icons/48x48/folder.png
new file mode 100644
index 000000000..dee3078d5
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/folder.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/plus.png b/dist/qt_themes/qdarkstyle/icons/48x48/plus.png
new file mode 100644
index 000000000..16cc8b4f4
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/plus.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png
new file mode 100644
index 000000000..9052ae794
Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/sd_card.png differ
diff --git a/dist/qt_themes/qdarkstyle/icons/index.theme b/dist/qt_themes/qdarkstyle/icons/index.theme
index 558ece40b..d1e12f3ef 100644
--- a/dist/qt_themes/qdarkstyle/icons/index.theme
+++ b/dist/qt_themes/qdarkstyle/icons/index.theme
@@ -2,10 +2,13 @@
 Name=qdarkstyle
 Comment=dark theme
 Inherits=default
-Directories=16x16,256x256
+Directories=16x16,48x48,256x256
  
 [16x16]
 Size=16
- 
+
+[48x48]
+Size=48
+
 [256x256]
 Size=256
\ No newline at end of file
diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc
index 54a96b680..c2c14c28a 100644
--- a/dist/qt_themes/qdarkstyle/style.qrc
+++ b/dist/qt_themes/qdarkstyle/style.qrc
@@ -2,6 +2,12 @@
   <qresource prefix="icons/qdarkstyle">
     <file alias="index.theme">icons/index.theme</file>
     <file alias="16x16/lock.png">icons/16x16/lock.png</file>
+    <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
+    <file alias="48x48/chip.png">icons/48x48/chip.png</file>
+    <file alias="48x48/folder.png">icons/48x48/folder.png</file>
+    <file alias="48x48/plus.png">icons/48x48/plus.png</file>
+    <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
+    <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
   </qresource>
   <qresource prefix="qss_icons">
     <file>rc/up_arrow_disabled.png</file>
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index e5d252c81..ce80a727d 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -207,8 +207,34 @@ void Config::ReadValues() {
     qt_config->beginGroup("Paths");
     UISettings::values.roms_path = qt_config->value("romsPath").toString();
     UISettings::values.symbols_path = qt_config->value("symbolsPath").toString();
-    UISettings::values.gamedir = qt_config->value("gameListRootDir", ".").toString();
-    UISettings::values.gamedir_deepscan = qt_config->value("gameListDeepScan", false).toBool();
+    UISettings::values.game_dir_deprecated = qt_config->value("gameListRootDir", ".").toString();
+    UISettings::values.game_dir_deprecated_deepscan =
+        qt_config->value("gameListDeepScan", false).toBool();
+    int size = qt_config->beginReadArray("gamedirs");
+    for (int i = 0; i < size; ++i) {
+        qt_config->setArrayIndex(i);
+        UISettings::GameDir game_dir;
+        game_dir.path = qt_config->value("path").toString();
+        game_dir.deep_scan = qt_config->value("deep_scan", false).toBool();
+        game_dir.expanded = qt_config->value("expanded", true).toBool();
+        UISettings::values.game_dirs.append(game_dir);
+    }
+    qt_config->endArray();
+    // create NAND and SD card directories if empty, these are not removable through the UI, also
+    // carries over old game list settings if present
+    if (UISettings::values.game_dirs.isEmpty()) {
+        UISettings::GameDir game_dir;
+        game_dir.path = "INSTALLED";
+        game_dir.expanded = true;
+        UISettings::values.game_dirs.append(game_dir);
+        game_dir.path = "SYSTEM";
+        UISettings::values.game_dirs.append(game_dir);
+        if (UISettings::values.game_dir_deprecated != ".") {
+            game_dir.path = UISettings::values.game_dir_deprecated;
+            game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan;
+            UISettings::values.game_dirs.append(game_dir);
+        }
+    }
     UISettings::values.recent_files = qt_config->value("recentFiles").toStringList();
     UISettings::values.language = qt_config->value("language", "").toString();
     qt_config->endGroup();
@@ -386,8 +412,15 @@ void Config::SaveValues() {
     qt_config->beginGroup("Paths");
     qt_config->setValue("romsPath", UISettings::values.roms_path);
     qt_config->setValue("symbolsPath", UISettings::values.symbols_path);
-    qt_config->setValue("gameListRootDir", UISettings::values.gamedir);
-    qt_config->setValue("gameListDeepScan", UISettings::values.gamedir_deepscan);
+    qt_config->beginWriteArray("gamedirs");
+    for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) {
+        qt_config->setArrayIndex(i);
+        const auto& game_dir = UISettings::values.game_dirs.at(i);
+        qt_config->setValue("path", game_dir.path);
+        qt_config->setValue("deep_scan", game_dir.deep_scan);
+        qt_config->setValue("expanded", game_dir.expanded);
+    }
+    qt_config->endArray();
     qt_config->setValue("recentFiles", UISettings::values.recent_files);
     qt_config->setValue("language", UISettings::values.language);
     qt_config->endGroup();
diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp
index ad008a011..0bdeeeb3d 100644
--- a/src/citra_qt/configuration/configure_general.cpp
+++ b/src/citra_qt/configuration/configure_general.cpp
@@ -44,7 +44,6 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
 ConfigureGeneral::~ConfigureGeneral() {}
 
 void ConfigureGeneral::setConfiguration() {
-    ui->toggle_deepscan->setChecked(UISettings::values.gamedir_deepscan);
     ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
     ui->toggle_cpu_jit->setChecked(Settings::values.use_cpu_jit);
 
@@ -60,7 +59,6 @@ void ConfigureGeneral::setConfiguration() {
 }
 
 void ConfigureGeneral::applyConfiguration() {
-    UISettings::values.gamedir_deepscan = ui->toggle_deepscan->isChecked();
     UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
     UISettings::values.theme =
         ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString();
diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui
index c2bf24b52..7a949979a 100644
--- a/src/citra_qt/configuration/configure_general.ui
+++ b/src/citra_qt/configuration/configure_general.ui
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>345</width>
-    <height>493</height>
+    <height>504</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -31,13 +31,6 @@
             </property>
            </widget>
           </item>
-          <item>
-           <widget class="QCheckBox" name="toggle_deepscan">
-            <property name="text">
-             <string>Search sub-directories for games</string>
-            </property>
-           </widget>
-          </item>
           <item>
            <layout class="QHBoxLayout" name="horizontalLayout_2">
             <item>
diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp
index aa08bcf59..e01c302b7 100644
--- a/src/citra_qt/game_list.cpp
+++ b/src/citra_qt/game_list.cpp
@@ -43,7 +43,6 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e
         return QObject::eventFilter(obj, event);
 
     QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
-    int rowCount = gamelist->tree_view->model()->rowCount();
     QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower();
 
     // If the searchfield's text hasn't changed special function keys get checked
@@ -65,19 +64,9 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e
         // If there is only one result launch this game
         case Qt::Key_Return:
         case Qt::Key_Enter: {
-            QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view);
-            QModelIndex root_index = item_model->invisibleRootItem()->index();
-            QStandardItem* child_file;
-            QString file_path;
-            int resultCount = 0;
-            for (int i = 0; i < rowCount; ++i) {
-                if (!gamelist->tree_view->isRowHidden(i, root_index)) {
-                    ++resultCount;
-                    child_file = gamelist->item_model->item(i, 0);
-                    file_path = child_file->data(GameListItemPath::FullPathRole).toString();
-                }
-            }
-            if (resultCount == 1) {
+            if (gamelist->search_field->visible == 1) {
+                QString file_path = gamelist->getLastFilterResultItem();
+
                 // To avoid loading error dialog loops while confirming them using enter
                 // Also users usually want to run a diffrent game after closing one
                 gamelist->search_field->edit_filter->setText("");
@@ -97,6 +86,9 @@ bool GameList::SearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* e
 }
 
 void GameList::SearchField::setFilterResult(int visible, int total) {
+    this->visible = visible;
+    this->total = total;
+
     QString result_of_text = tr("of");
     QString result_text;
     if (total == 1) {
@@ -108,6 +100,25 @@ void GameList::SearchField::setFilterResult(int visible, int total) {
         QString("%1 %2 %3 %4").arg(visible).arg(result_of_text).arg(total).arg(result_text));
 }
 
+QString GameList::getLastFilterResultItem() {
+    QStandardItem* folder;
+    QStandardItem* child;
+    QString file_path;
+    int folderCount = item_model->rowCount();
+    for (int i = 0; i < folderCount; ++i) {
+        folder = item_model->item(i, 0);
+        QModelIndex folder_index = folder->index();
+        int childrenCount = folder->rowCount();
+        for (int j = 0; j < childrenCount; ++j) {
+            if (!tree_view->isRowHidden(j, folder_index)) {
+                child = folder->child(j, 0);
+                file_path = child->data(GameListItemPath::FullPathRole).toString();
+            }
+        }
+    }
+    return file_path;
+}
+
 void GameList::SearchField::clear() {
     edit_filter->setText("");
 }
@@ -161,45 +172,91 @@ bool GameList::containsAllWords(QString haystack, QString userinput) {
                        [haystack](QString s) { return haystack.contains(s); });
 }
 
+// Syncs the expanded state of Game Directories with settings to persist across sessions
+void GameList::onItemExpanded(const QModelIndex& item) {
+    GameListItemType type = item.data(GameListItem::TypeRole).value<GameListItemType>();
+    if (type == GameListItemType::CustomDir || type == GameListItemType::InstalledDir ||
+        type == GameListItemType::SystemDir)
+        item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded =
+            tree_view->isExpanded(item);
+}
+
 // Event in order to filter the gamelist after editing the searchfield
 void GameList::onTextChanged(const QString& newText) {
-    int rowCount = tree_view->model()->rowCount();
+    int folderCount = tree_view->model()->rowCount();
     QString edit_filter_text = newText.toLower();
-
+    QStandardItem* folder;
+    QStandardItem* child;
+    int childrenTotal = 0;
     QModelIndex root_index = item_model->invisibleRootItem()->index();
 
     // If the searchfield is empty every item is visible
     // Otherwise the filter gets applied
     if (edit_filter_text.isEmpty()) {
-        for (int i = 0; i < rowCount; ++i) {
-            tree_view->setRowHidden(i, root_index, false);
+        for (int i = 0; i < folderCount; ++i) {
+            folder = item_model->item(i, 0);
+            QModelIndex folder_index = folder->index();
+            int childrenCount = folder->rowCount();
+            for (int j = 0; j < childrenCount; ++j) {
+                ++childrenTotal;
+                tree_view->setRowHidden(j, folder_index, false);
+            }
         }
-        search_field->setFilterResult(rowCount, rowCount);
+        search_field->setFilterResult(childrenTotal, childrenTotal);
     } else {
-        QStandardItem* child_file;
         QString file_path, file_name, file_title, file_programmid;
         int result_count = 0;
-        for (int i = 0; i < rowCount; ++i) {
-            child_file = item_model->item(i, 0);
-            file_path = child_file->data(GameListItemPath::FullPathRole).toString().toLower();
-            file_name = file_path.mid(file_path.lastIndexOf("/") + 1);
-            file_title = child_file->data(GameListItemPath::TitleRole).toString().toLower();
-            file_programmid =
-                child_file->data(GameListItemPath::ProgramIdRole).toString().toLower();
+        for (int i = 0; i < folderCount; ++i) {
+            folder = item_model->item(i, 0);
+            QModelIndex folder_index = folder->index();
+            int childrenCount = folder->rowCount();
+            for (int j = 0; j < childrenCount; ++j) {
+                ++childrenTotal;
+                child = folder->child(j, 0);
+                file_path = child->data(GameListItemPath::FullPathRole).toString().toLower();
+                file_name = file_path.mid(file_path.lastIndexOf("/") + 1);
+                file_title = child->data(GameListItemPath::TitleRole).toString().toLower();
+                file_programmid = child->data(GameListItemPath::ProgramIdRole).toString().toLower();
 
-            // Only items which filename in combination with its title contains all words
-            // that are in the searchfield will be visible in the gamelist
-            // The search is case insensitive because of toLower()
-            // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
-            // multiple conversions of edit_filter_text for each game in the gamelist
-            if (containsAllWords(file_name.append(" ").append(file_title), edit_filter_text) ||
-                (file_programmid.count() == 16 && edit_filter_text.contains(file_programmid))) {
-                tree_view->setRowHidden(i, root_index, false);
-                ++result_count;
-            } else {
-                tree_view->setRowHidden(i, root_index, true);
+                // Only items which filename in combination with its title contains all words
+                // that are in the searchfield will be visible in the gamelist
+                // The search is case insensitive because of toLower()
+                // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent
+                // multiple conversions of edit_filter_text for each game in the gamelist
+                if (containsAllWords(file_name.append(" ").append(file_title), edit_filter_text) ||
+                    (file_programmid.count() == 16 && edit_filter_text.contains(file_programmid))) {
+                    tree_view->setRowHidden(j, folder_index, false);
+                    ++result_count;
+                } else {
+                    tree_view->setRowHidden(j, folder_index, true);
+                }
+                search_field->setFilterResult(result_count, childrenTotal);
             }
-            search_field->setFilterResult(result_count, rowCount);
+        }
+    }
+}
+
+void GameList::onUpdateThemedIcons() {
+    for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) {
+        QStandardItem* child = item_model->invisibleRootItem()->child(i);
+
+        switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) {
+        case GameListItemType::InstalledDir:
+            child->setData(QIcon::fromTheme("sd_card").pixmap(48), Qt::DecorationRole);
+            break;
+        case GameListItemType::SystemDir:
+            child->setData(QIcon::fromTheme("chip").pixmap(48), Qt::DecorationRole);
+            break;
+        case GameListItemType::CustomDir: {
+            const UISettings::GameDir* game_dir =
+                child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
+            QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder";
+            child->setData(QIcon::fromTheme(icon_name).pixmap(48), Qt::DecorationRole);
+            break;
+        }
+        case GameListItemType::AddDir:
+            child->setData(QIcon::fromTheme("plus").pixmap(48), Qt::DecorationRole);
+            break;
         }
     }
 }
@@ -235,12 +292,16 @@ GameList::GameList(GMainWindow* parent) : QWidget{parent} {
     item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, "Region");
     item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type");
     item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size");
+    item_model->setSortRole(GameListItemPath::TitleRole);
 
+    connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons);
     connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry);
     connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu);
+    connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded);
+    connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded);
 
-    // We must register all custom types with the Qt Automoc system so that we are able to use it
-    // with signals/slots. In this case, QList falls under the umbrells of custom types.
+    // We must register all custom types with the Qt Automoc system so that we are able to use
+    // it with signals/slots. In this case, QList falls under the umbrells of custom types.
     qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
 
     layout->setContentsMargins(0, 0, 0, 0);
@@ -268,27 +329,57 @@ void GameList::clearFilter() {
     search_field->clear();
 }
 
-void GameList::AddEntry(const QList<QStandardItem*>& entry_items) {
+void GameList::AddDirEntry(GameListDir* entry_items) {
     item_model->invisibleRootItem()->appendRow(entry_items);
+    tree_view->setExpanded(
+        entry_items->index(),
+        entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded);
+}
+
+void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) {
+    parent->appendRow(entry_items);
 }
 
 void GameList::ValidateEntry(const QModelIndex& item) {
-    // We don't care about the individual QStandardItem that was selected, but its row.
-    int row = item_model->itemFromIndex(item)->row();
-    QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
-    QString file_path = child_file->data(GameListItemPath::FullPathRole).toString();
+    auto selected = item.sibling(item.row(), 0);
 
-    if (file_path.isEmpty())
-        return;
-    std::string std_file_path(file_path.toStdString());
-    if (!FileUtil::Exists(std_file_path) || FileUtil::IsDirectory(std_file_path))
-        return;
-    // Users usually want to run a diffrent game after closing one
-    search_field->clear();
-    emit GameChosen(file_path);
+    switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
+    case GameListItemType::Game: {
+        QString file_path = selected.data(GameListItemPath::FullPathRole).toString();
+        if (file_path.isEmpty())
+            return;
+        QFileInfo file_info(file_path);
+        if (!file_info.exists() || file_info.isDir())
+            return;
+        // Users usually want to run a different game after closing one
+        search_field->clear();
+        emit GameChosen(file_path);
+        break;
+    }
+    case GameListItemType::AddDir:
+        emit AddDirectory();
+        break;
+    }
+}
+
+bool GameList::isEmpty() {
+    for (int i = 0; i < item_model->rowCount(); i++) {
+        const QStandardItem* child = item_model->invisibleRootItem()->child(i);
+        GameListItemType type = static_cast<GameListItemType>(child->type());
+        if (!child->hasChildren() &&
+            (type == GameListItemType::InstalledDir || type == GameListItemType::SystemDir)) {
+            item_model->invisibleRootItem()->removeRow(child->row());
+            i--;
+        };
+    }
+    return !item_model->invisibleRootItem()->hasChildren();
 }
 
 void GameList::DonePopulating(QStringList watch_list) {
+    emit ShowList(!isEmpty());
+
+    item_model->invisibleRootItem()->appendRow(new GameListAddDir());
+
     // Clear out the old directories to watch for changes and add the new ones
     auto watch_dirs = watcher->directories();
     if (!watch_dirs.isEmpty()) {
@@ -305,9 +396,16 @@ void GameList::DonePopulating(QStringList watch_list) {
         QCoreApplication::processEvents();
     }
     tree_view->setEnabled(true);
-    int rowCount = tree_view->model()->rowCount();
-    search_field->setFilterResult(rowCount, rowCount);
-    if (rowCount > 0) {
+    int folderCount = tree_view->model()->rowCount();
+    int childrenTotal = 0;
+    for (int i = 0; i < folderCount; ++i) {
+        int childrenCount = item_model->item(i, 0)->rowCount();
+        for (int j = 0; j < childrenCount; ++j) {
+            ++childrenTotal;
+        }
+    }
+    search_field->setFilterResult(childrenTotal, childrenTotal);
+    if (childrenTotal > 0) {
         search_field->setFocus();
     }
 }
@@ -317,12 +415,25 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
     if (!item.isValid())
         return;
 
-    int row = item_model->itemFromIndex(item)->row();
-    QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME);
-    u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong();
-
+    auto selected = item.sibling(item.row(), 0);
     QMenu context_menu;
+    switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
+    case GameListItemType::Game:
+        AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong());
+        break;
+    case GameListItemType::CustomDir:
+        AddPermDirPopup(context_menu, selected);
+        AddCustomDirPopup(context_menu, selected);
+        break;
+    case GameListItemType::InstalledDir:
+    case GameListItemType::SystemDir:
+        AddPermDirPopup(context_menu, selected);
+        break;
+    }
+    context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
+}
 
+void GameList::AddGamePopup(QMenu& context_menu, u64 program_id) {
     QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
     QAction* open_application_location = context_menu.addAction(tr("Open Application Location"));
     QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location"));
@@ -341,16 +452,81 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
         });
     navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
 
-    connect(open_save_location, &QAction::triggered,
-            [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); });
-    connect(open_application_location, &QAction::triggered,
-            [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); });
-    connect(open_update_location, &QAction::triggered,
-            [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA); });
-    connect(navigate_to_gamedb_entry, &QAction::triggered,
-            [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); });
+    connect(open_save_location, &QAction::triggered, [this, program_id] {
+        emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA);
+    });
+    connect(open_application_location, &QAction::triggered, [this, program_id] {
+        emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION);
+    });
+    connect(open_update_location, &QAction::triggered, [this, program_id] {
+        emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA);
+    });
+    connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
+        emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
+    });
+};
 
-    context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location));
+void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
+    UISettings::GameDir& game_dir =
+        *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
+
+    QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders"));
+    QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory"));
+
+    deep_scan->setCheckable(true);
+    deep_scan->setChecked(game_dir.deep_scan);
+
+    connect(deep_scan, &QAction::triggered, [this, &game_dir] {
+        game_dir.deep_scan = !game_dir.deep_scan;
+        PopulateAsync(UISettings::values.game_dirs);
+    });
+    connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] {
+        UISettings::values.game_dirs.removeOne(game_dir);
+        item_model->invisibleRootItem()->removeRow(selected.row());
+    });
+}
+
+void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
+    UISettings::GameDir& game_dir =
+        *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>();
+
+    QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up"));
+    QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down "));
+    QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location"));
+
+    int row = selected.row();
+
+    move_up->setEnabled(row > 0);
+    move_down->setEnabled(row < item_model->rowCount() - 2);
+
+    connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] {
+        // find the indices of the items in settings and swap them
+        UISettings::values.game_dirs.swap(
+            UISettings::values.game_dirs.indexOf(game_dir),
+            UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() - 1, 0)
+                                                      .data(GameListDir::GameDirRole)
+                                                      .value<UISettings::GameDir*>()));
+        // move the treeview items
+        QList<QStandardItem*> item = item_model->takeRow(row);
+        item_model->invisibleRootItem()->insertRow(row - 1, item);
+        tree_view->setExpanded(selected, game_dir.expanded);
+    });
+
+    connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] {
+        // find the indices of the items in settings and swap them
+        UISettings::values.game_dirs.swap(
+            UISettings::values.game_dirs.indexOf(game_dir),
+            UISettings::values.game_dirs.indexOf(*selected.sibling(selected.row() + 1, 0)
+                                                      .data(GameListDir::GameDirRole)
+                                                      .value<UISettings::GameDir*>()));
+        // move the treeview items
+        QList<QStandardItem*> item = item_model->takeRow(row);
+        item_model->invisibleRootItem()->insertRow(row + 1, item);
+        tree_view->setExpanded(selected, game_dir.expanded);
+    });
+
+    connect(open_directory_location, &QAction::triggered,
+            [this, game_dir] { emit OpenDirectory(game_dir.path); });
 }
 
 void GameList::LoadCompatibilityList() {
@@ -399,27 +575,23 @@ QStandardItemModel* GameList::GetModel() const {
     return item_model;
 }
 
-void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) {
-    if (!FileUtil::Exists(dir_path.toStdString()) ||
-        !FileUtil::IsDirectory(dir_path.toStdString())) {
-        NGLOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString());
-        search_field->setFilterResult(0, 0);
-        return;
-    }
-
+void GameList::PopulateAsync(QList<UISettings::GameDir>& game_dirs) {
     tree_view->setEnabled(false);
     // Delete any rows that might already exist if we're repopulating
     item_model->removeRows(0, item_model->rowCount());
+    search_field->clear();
 
     emit ShouldCancelWorker();
 
-    GameListWorker* worker = new GameListWorker(dir_path, deep_scan, compatibility_list);
+    GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list);
 
     connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
+    connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
+            Qt::QueuedConnection);
     connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating,
             Qt::QueuedConnection);
-    // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel
-    // without delay.
+    // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to
+    // cancel without delay.
     connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel,
             Qt::DirectConnection);
 
@@ -451,16 +623,17 @@ static bool HasSupportedFileExtension(const std::string& file_name) {
 }
 
 void GameList::RefreshGameDirectory() {
-    if (!UISettings::values.gamedir.isEmpty() && current_worker != nullptr) {
+    if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) {
         NGLOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
-        search_field->clear();
-        PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
+        PopulateAsync(UISettings::values.game_dirs);
     }
 }
 
-void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) {
-    const auto callback = [this, recursion](unsigned* num_entries_out, const std::string& directory,
-                                            const std::string& virtual_name) -> bool {
+void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
+                                             GameListDir* parent_dir) {
+    const auto callback = [this, recursion, parent_dir](unsigned* num_entries_out,
+                                                        const std::string& directory,
+                                                        const std::string& virtual_name) -> bool {
         std::string physical_name = directory + DIR_SEP + virtual_name;
 
         if (stop_processing)
@@ -510,17 +683,20 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
             if (it != compatibility_list.end())
                 compatibility = it->second.first;
 
-            emit EntryReady({
-                new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id),
-                new GameListItemCompat(compatibility),
-                new GameListItemRegion(smdh),
-                new GameListItem(
-                    QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
-                new GameListItemSize(FileUtil::GetSize(physical_name)),
-            });
+            emit EntryReady(
+                {
+                    new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id),
+                    new GameListItemCompat(compatibility),
+                    new GameListItemRegion(smdh),
+                    new GameListItem(
+                        QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
+                    new GameListItemSize(FileUtil::GetSize(physical_name)),
+                },
+                parent_dir);
+
         } else if (is_dir && recursion > 0) {
             watch_list.append(QString::fromStdString(physical_name));
-            AddFstEntriesToGameList(physical_name, recursion - 1);
+            AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir);
         }
 
         return true;
@@ -531,27 +707,33 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
 
 void GameListWorker::run() {
     stop_processing = false;
-    watch_list.append(dir_path);
-    watch_list.append(QString::fromStdString(
-        std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
-        "Nintendo "
-        "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000"));
-    watch_list.append(QString::fromStdString(
-        std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
-        "Nintendo "
-        "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/0004000e"));
-    watch_list.append(
-        QString::fromStdString(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
-                               "00000000000000000000000000000000/title/00040010"));
-    AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
-    AddFstEntriesToGameList(
-        std::string(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
-            "Nintendo "
-            "3DS/00000000000000000000000000000000/00000000000000000000000000000000/title/00040000",
-        2);
-    AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
-                                "00000000000000000000000000000000/title/00040010",
-                            2);
+    for (UISettings::GameDir& game_dir : game_dirs) {
+        if (game_dir.path == "INSTALLED") {
+            QString path = QString(FileUtil::GetUserPath(D_SDMC_IDX).c_str()) +
+                           "Nintendo "
+                           "3DS/00000000000000000000000000000000/"
+                           "00000000000000000000000000000000/title/00040000";
+            watch_list.append(path);
+            GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
+            emit DirEntryReady({game_list_dir});
+            AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir);
+        } else if (game_dir.path == "SYSTEM") {
+            QString path = QString(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
+                           "00000000000000000000000000000000/title/00040010";
+            watch_list.append(path);
+            GameListDir* game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
+            emit DirEntryReady({game_list_dir});
+            AddFstEntriesToGameList(std::string(FileUtil::GetUserPath(D_NAND_IDX).c_str()) +
+                                        "00000000000000000000000000000000/title/00040010",
+                                    2, game_list_dir);
+        } else {
+            watch_list.append(game_dir.path);
+            GameListDir* game_list_dir = new GameListDir(game_dir);
+            emit DirEntryReady({game_list_dir});
+            AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0,
+                                    game_list_dir);
+        }
+    };
     emit Finished(watch_list);
 }
 
@@ -559,3 +741,37 @@ void GameListWorker::Cancel() {
     this->disconnect();
     stop_processing = true;
 }
+
+GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
+    this->main_window = parent;
+
+    connect(main_window, &GMainWindow::UpdateThemedIcons, this,
+            &GameListPlaceholder::onUpdateThemedIcons);
+
+    layout = new QVBoxLayout;
+    image = new QLabel;
+    text = new QLabel;
+    layout->setAlignment(Qt::AlignCenter);
+    image->setPixmap(QIcon::fromTheme("plus_folder").pixmap(200));
+
+    text->setText(tr("Double-click to add a new folder to the game list "));
+    QFont font = text->font();
+    font.setPointSize(20);
+    text->setFont(font);
+    text->setAlignment(Qt::AlignHCenter);
+    image->setAlignment(Qt::AlignHCenter);
+
+    layout->addWidget(image);
+    layout->addWidget(text);
+    setLayout(layout);
+}
+
+GameListPlaceholder::~GameListPlaceholder() = default;
+
+void GameListPlaceholder::onUpdateThemedIcons() {
+    image->setPixmap(QIcon::fromTheme("plus_folder").pixmap(200));
+}
+
+void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) {
+    emit GameListPlaceholder::AddDirectory();
+}
diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h
index 91a0997dc..f600f6ef8 100644
--- a/src/citra_qt/game_list.h
+++ b/src/citra_qt/game_list.h
@@ -8,13 +8,17 @@
 #include <QString>
 #include <QWidget>
 #include "common/common_types.h"
+#include "ui_settings.h"
 
 class GameListWorker;
+class GameListDir;
 class GMainWindow;
 class QFileSystemWatcher;
 class QHBoxLayout;
 class QLabel;
 class QLineEdit;
+template <typename>
+class QList;
 class QModelIndex;
 class QStandardItem;
 class QStandardItemModel;
@@ -39,10 +43,14 @@ public:
 
     class SearchField : public QWidget {
     public:
+        explicit SearchField(GameList* parent = nullptr);
+
         void setFilterResult(int visible, int total);
         void clear();
         void setFocus();
-        explicit SearchField(GameList* parent = nullptr);
+
+        int visible;
+        int total;
 
     private:
         class KeyReleaseEater : public QObject {
@@ -67,12 +75,14 @@ public:
     explicit GameList(GMainWindow* parent = nullptr);
     ~GameList() override;
 
+    QString getLastFilterResultItem();
     void clearFilter();
     void setFilterFocus();
     void setFilterVisible(bool visibility);
+    bool isEmpty();
 
     void LoadCompatibilityList();
-    void PopulateAsync(const QString& dir_path, bool deep_scan);
+    void PopulateAsync(QList<UISettings::GameDir>& game_dirs);
 
     void SaveInterfaceLayout();
     void LoadInterfaceLayout();
@@ -88,20 +98,30 @@ signals:
     void NavigateToGamedbEntryRequested(
         u64 program_id,
         std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
+    void OpenDirectory(QString directory);
+    void AddDirectory();
+    void ShowList(bool show);
 
 private slots:
+    void onItemExpanded(const QModelIndex& item);
     void onTextChanged(const QString& newText);
     void onFilterCloseClicked();
+    void onUpdateThemedIcons();
 
 private:
-    void AddEntry(const QList<QStandardItem*>& entry_items);
+    void AddDirEntry(GameListDir* entry_items);
+    void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent);
     void ValidateEntry(const QModelIndex& item);
     void DonePopulating(QStringList watch_list);
 
-    void PopupContextMenu(const QPoint& menu_location);
     void RefreshGameDirectory();
     bool containsAllWords(QString haystack, QString userinput);
 
+    void PopupContextMenu(const QPoint& menu_location);
+    void AddGamePopup(QMenu& context_menu, u64 program_id);
+    void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
+    void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
+
     SearchField* search_field;
     GMainWindow* main_window = nullptr;
     QVBoxLayout* layout = nullptr;
@@ -113,3 +133,25 @@ private:
 };
 
 Q_DECLARE_METATYPE(GameListOpenTarget);
+
+class GameListPlaceholder : public QWidget {
+    Q_OBJECT
+public:
+    explicit GameListPlaceholder(GMainWindow* parent = nullptr);
+    ~GameListPlaceholder();
+
+signals:
+    void AddDirectory();
+
+private slots:
+    void onUpdateThemedIcons();
+
+protected:
+    void mouseDoubleClickEvent(QMouseEvent* event) override;
+
+private:
+    GMainWindow* main_window = nullptr;
+    QVBoxLayout* layout = nullptr;
+    QLabel* image = nullptr;
+    QLabel* text = nullptr;
+};
diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h
index a9341de00..8e5298cb3 100644
--- a/src/citra_qt/game_list_p.h
+++ b/src/citra_qt/game_list_p.h
@@ -8,18 +8,30 @@
 #include <map>
 #include <unordered_map>
 #include <QCoreApplication>
+#include <QFileInfo>
 #include <QImage>
 #include <QObject>
 #include <QPainter>
 #include <QRunnable>
 #include <QStandardItem>
 #include <QString>
+#include "citra_qt/ui_settings.h"
 #include "citra_qt/util/util.h"
 #include "common/file_util.h"
 #include "common/logging/log.h"
 #include "common/string_util.h"
 #include "core/loader/smdh.h"
 
+enum class GameListItemType {
+    Game = QStandardItem::UserType + 1,
+    CustomDir = QStandardItem::UserType + 2,
+    InstalledDir = QStandardItem::UserType + 3,
+    SystemDir = QStandardItem::UserType + 4,
+    AddDir = QStandardItem::UserType + 5
+};
+
+Q_DECLARE_METATYPE(GameListItemType);
+
 /**
  * Gets the game icon from SMDH data.
  * @param smdh SMDH data
@@ -126,8 +138,13 @@ const static inline std::map<QString, CompatStatus> status_data = {
 
 class GameListItem : public QStandardItem {
 public:
+    // used to access type from item index
+    static const int TypeRole = Qt::UserRole + 1;
+    static const int SortRole = Qt::UserRole + 2;
     GameListItem() : QStandardItem() {}
-    GameListItem(const QString& string) : QStandardItem(string) {}
+    GameListItem(const QString& string) : QStandardItem(string) {
+        setData(string, SortRole);
+    }
     virtual ~GameListItem() override {}
 };
 
@@ -139,13 +156,14 @@ public:
  */
 class GameListItemPath : public GameListItem {
 public:
-    static const int FullPathRole = Qt::UserRole + 1;
-    static const int TitleRole = Qt::UserRole + 2;
-    static const int ProgramIdRole = Qt::UserRole + 3;
+    static const int TitleRole = SortRole;
+    static const int FullPathRole = SortRole + 1;
+    static const int ProgramIdRole = SortRole + 2;
 
     GameListItemPath() : GameListItem() {}
     GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id)
         : GameListItem() {
+        setData(type(), TypeRole);
         setData(game_path, FullPathRole);
         setData(qulonglong(program_id), ProgramIdRole);
 
@@ -166,6 +184,10 @@ public:
                 TitleRole);
     }
 
+    int type() const override {
+        return static_cast<int>(GameListItemType::Game);
+    }
+
     QVariant data(int role) const override {
         if (role == Qt::DisplayRole) {
             std::string path, filename, extension;
@@ -202,9 +224,12 @@ public:
 
 class GameListItemCompat : public GameListItem {
 public:
-    static const int CompatNumberRole = Qt::UserRole + 1;
+    static const int CompatNumberRole = SortRole;
+
     GameListItemCompat() = default;
     explicit GameListItemCompat(const QString compatiblity) {
+        setData(type(), TypeRole);
+
         auto iterator = status_data.find(compatiblity);
         if (iterator == status_data.end()) {
             NGLOG_WARNING(Frontend, "Invalid compatibility number {}", compatiblity.toStdString());
@@ -217,6 +242,10 @@ public:
         setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
     }
 
+    int type() const override {
+        return static_cast<int>(GameListItemType::Game);
+    }
+
     bool operator<(const QStandardItem& other) const override {
         return data(CompatNumberRole) < other.data(CompatNumberRole);
     }
@@ -226,6 +255,8 @@ class GameListItemRegion : public GameListItem {
 public:
     GameListItemRegion() = default;
     explicit GameListItemRegion(const std::vector<u8>& smdh_data) {
+        setData(type(), TypeRole);
+
         if (!Loader::IsValidSMDH(smdh_data)) {
             setText(QObject::tr("Invalid region"));
             return;
@@ -235,6 +266,11 @@ public:
         memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
 
         setText(GetRegionFromSMDH(smdh));
+        setData(GetRegionFromSMDH(smdh), SortRole);
+    }
+
+    int type() const override {
+        return static_cast<int>(GameListItemType::Game);
     }
 };
 
@@ -245,10 +281,11 @@ public:
  */
 class GameListItemSize : public GameListItem {
 public:
-    static const int SizeRole = Qt::UserRole + 1;
+    static const int SizeRole = SortRole;
 
     GameListItemSize() : GameListItem() {}
     GameListItemSize(const qulonglong size_bytes) : GameListItem() {
+        setData(type(), TypeRole);
         setData(size_bytes, SizeRole);
     }
 
@@ -264,6 +301,10 @@ public:
         }
     }
 
+    int type() const override {
+        return static_cast<int>(GameListItemType::Game);
+    }
+
     /**
      * This operator is, in practice, only used by the TreeView sorting systems.
      * Override it so that it will correctly sort by numerical value instead of by string
@@ -274,6 +315,55 @@ public:
     }
 };
 
+class GameListDir : public GameListItem {
+public:
+    static const int GameDirRole = Qt::UserRole + 2;
+
+    explicit GameListDir(UISettings::GameDir& directory,
+                         GameListItemType dir_type = GameListItemType::CustomDir)
+        : dir_type{dir_type} {
+        setData(type(), TypeRole);
+
+        UISettings::GameDir* game_dir = &directory;
+        setData(QVariant::fromValue(game_dir), GameDirRole);
+        switch (dir_type) {
+        case GameListItemType::InstalledDir:
+            setData(QIcon::fromTheme("sd_card").pixmap(48), Qt::DecorationRole);
+            setData("Installed Titles", Qt::DisplayRole);
+            break;
+        case GameListItemType::SystemDir:
+            setData(QIcon::fromTheme("chip").pixmap(48), Qt::DecorationRole);
+            setData("System Titles", Qt::DisplayRole);
+            break;
+        case GameListItemType::CustomDir:
+            QString icon_name = QFileInfo::exists(game_dir->path) ? "folder" : "bad_folder";
+            setData(QIcon::fromTheme(icon_name).pixmap(48), Qt::DecorationRole);
+            setData(game_dir->path, Qt::DisplayRole);
+            break;
+        };
+    };
+
+    int type() const override {
+        return static_cast<int>(dir_type);
+    }
+
+private:
+    GameListItemType dir_type;
+};
+
+class GameListAddDir : public GameListItem {
+public:
+    explicit GameListAddDir() {
+        setData(type(), TypeRole);
+        setData(QIcon::fromTheme("plus").pixmap(48), Qt::DecorationRole);
+        setData("Add New Game Directory", Qt::DisplayRole);
+    }
+
+    int type() const override {
+        return static_cast<int>(GameListItemType::AddDir);
+    }
+};
+
 /**
  * Asynchronous worker object for populating the game list.
  * Communicates with other threads through Qt's signal/slot system.
@@ -282,11 +372,10 @@ class GameListWorker : public QObject, public QRunnable {
     Q_OBJECT
 
 public:
-    GameListWorker(
-        QString dir_path, bool deep_scan,
+    explicit GameListWorker(
+        QList<UISettings::GameDir>& game_dirs,
         const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list)
-        : QObject(), QRunnable(), dir_path(dir_path), deep_scan(deep_scan),
-          compatibility_list(compatibility_list) {}
+        : QObject(), QRunnable(), game_dirs(game_dirs), compatibility_list(compatibility_list) {}
 
 public slots:
     /// Starts the processing of directory tree information.
@@ -298,22 +387,24 @@ signals:
     /**
      * The `EntryReady` signal is emitted once an entry has been prepared and is ready
      * to be added to the game list.
-     * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry.
+     * @param entry_items a list with `QStandardItem`s that make up the columns of the new
+     * entry.
      */
-    void EntryReady(QList<QStandardItem*> entry_items);
+    void DirEntryReady(GameListDir* entry_items);
+    void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir);
 
     /**
-     * After the worker has traversed the game directory looking for entries, this signal is emmited
-     * with a list of folders that should be watched for changes as well.
+     * After the worker has traversed the game directory looking for entries, this signal is
+     * emitted with a list of folders that should be watched for changes as well.
      */
     void Finished(QStringList watch_list);
 
 private:
     QStringList watch_list;
-    QString dir_path;
-    bool deep_scan;
     const std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list;
+    QList<UISettings::GameDir>& game_dirs;
     std::atomic_bool stop_processing;
 
-    void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);
+    void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
+                                 GameListDir* parent_dir);
 };
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 11f1dc478..5bfc0c969 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -143,7 +143,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
     show();
 
     game_list->LoadCompatibilityList();
-    game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
+    game_list->PopulateAsync(UISettings::values.game_dirs);
 
     // Show one-time "callout" messages to the user
     ShowCallouts();
@@ -177,6 +177,10 @@ void GMainWindow::InitializeWidgets() {
     game_list = new GameList(this);
     ui.horizontalLayout->addWidget(game_list);
 
+    game_list_placeholder = new GameListPlaceholder(this);
+    ui.horizontalLayout->addWidget(game_list_placeholder);
+    game_list_placeholder->setVisible(false);
+
     multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui.action_Leave_Room,
                                              ui.action_Show_Room);
     multiplayer_state->setVisible(false);
@@ -415,9 +419,14 @@ void GMainWindow::RestoreUIState() {
 
 void GMainWindow::ConnectWidgetEvents() {
     connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
+    connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
     connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
     connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
             &GMainWindow::OnGameListNavigateToGamedbEntry);
+    connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory);
+    connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
+            &GMainWindow::OnGameListAddDirectory);
+    connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
 
     connect(this, &GMainWindow::EmulationStarting, render_window,
             &GRenderWindow::OnEmulationStarting);
@@ -435,8 +444,6 @@ void GMainWindow::ConnectMenuEvents() {
     // File
     connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile);
     connect(ui.action_Install_CIA, &QAction::triggered, this, &GMainWindow::OnMenuInstallCIA);
-    connect(ui.action_Select_Game_List_Root, &QAction::triggered, this,
-            &GMainWindow::OnMenuSelectGameListRoot);
     connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close);
 
     // Emulation
@@ -688,6 +695,7 @@ void GMainWindow::BootGame(const QString& filename) {
     registersWidget->OnDebugModeEntered();
     if (ui.action_Single_Window_Mode->isChecked()) {
         game_list->hide();
+        game_list_placeholder->hide();
     }
     status_bar_update_timer.start(2000);
 
@@ -729,7 +737,10 @@ void GMainWindow::ShutdownGame() {
     ui.action_Stop->setEnabled(false);
     ui.action_Report_Compatibility->setEnabled(false);
     render_window->hide();
-    game_list->show();
+    if (game_list->isEmpty())
+        game_list_placeholder->show();
+    else
+        game_list->show();
     game_list->setFilterFocus();
 
     // Disable status bar updates
@@ -844,6 +855,48 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(
     QDesktopServices::openUrl(QUrl("https://citra-emu.org/game/" + directory));
 }
 
+void GMainWindow::OnGameListOpenDirectory(QString directory) {
+    QString path;
+    if (directory == "INSTALLED") {
+        path =
+            QString::fromStdString(FileUtil::GetUserPath(D_SDMC_IDX).c_str() +
+                                   std::string("Nintendo "
+                                               "3DS/00000000000000000000000000000000/"
+                                               "00000000000000000000000000000000/title/00040000"));
+    } else if (directory == "SYSTEM") {
+        path =
+            QString::fromStdString(FileUtil::GetUserPath(D_NAND_IDX).c_str() +
+                                   std::string("00000000000000000000000000000000/title/00040010"));
+    } else {
+        path = directory;
+    }
+    if (!QFileInfo::exists(path)) {
+        QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!"));
+        return;
+    }
+    QDesktopServices::openUrl(QUrl::fromLocalFile(path));
+}
+
+void GMainWindow::OnGameListAddDirectory() {
+    QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
+    if (dir_path.isEmpty())
+        return;
+    UISettings::GameDir game_dir{dir_path, false, true};
+    if (!UISettings::values.game_dirs.contains(game_dir)) {
+        UISettings::values.game_dirs.append(game_dir);
+        game_list->PopulateAsync(UISettings::values.game_dirs);
+    } else {
+        NGLOG_WARNING(Frontend, "Selected directory is already in the game list");
+    }
+}
+
+void GMainWindow::OnGameListShowList(bool show) {
+    if (emulation_running && ui.action_Single_Window_Mode->isChecked())
+        return;
+    game_list->setVisible(show);
+    game_list_placeholder->setVisible(!show);
+};
+
 void GMainWindow::OnMenuLoadFile() {
     QString extensions;
     for (const auto& piece : game_list->supported_file_extensions)
@@ -861,14 +914,6 @@ void GMainWindow::OnMenuLoadFile() {
     }
 }
 
-void GMainWindow::OnMenuSelectGameListRoot() {
-    QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory"));
-    if (!dir_path.isEmpty()) {
-        UISettings::values.gamedir = dir_path;
-        game_list->PopulateAsync(dir_path, UISettings::values.gamedir_deepscan);
-    }
-}
-
 void GMainWindow::OnMenuInstallCIA() {
     QStringList filepaths = QFileDialog::getOpenFileNames(
         this, tr("Load Files"), UISettings::values.roms_path,
@@ -1105,6 +1150,7 @@ void GMainWindow::OnConfigure() {
     if (result == QDialog::Accepted) {
         configureDialog.applyConfiguration();
         UpdateUITheme();
+        emit UpdateThemedIcons();
         SyncMenuUISettings();
         config->Save();
     }
@@ -1324,7 +1370,6 @@ void GMainWindow::UpdateUITheme() {
         QIcon::setThemeName(":/icons/default");
     }
     QIcon::setThemeSearchPaths(theme_paths);
-    emit UpdateThemedIcons();
 }
 
 void GMainWindow::LoadTranslation() {
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 4ec33cc0d..edb893168 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -20,6 +20,7 @@ class ClickableLabel;
 class EmuThread;
 class GameList;
 enum class GameListOpenTarget;
+class GameListPlaceholder;
 class GImageInfo;
 class GPUCommandListWidget;
 class GPUCommandStreamWidget;
@@ -148,13 +149,14 @@ private slots:
     void OnGameListNavigateToGamedbEntry(
         u64 program_id,
         std::unordered_map<std::string, std::pair<QString, QString>>& compatibility_list);
+    void OnGameListOpenDirectory(QString path);
+    void OnGameListAddDirectory();
+    void OnGameListShowList(bool show);
     void OnMenuLoadFile();
     void OnMenuInstallCIA();
     void OnUpdateProgress(size_t written, size_t total);
     void OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath);
     void OnCIAInstallFinished();
-    /// Called whenever a user selects the "File->Select Game List Root" menu item
-    void OnMenuSelectGameListRoot();
     void OnMenuRecentFile();
     void OnConfigure();
     void OnToggleFilterBar();
@@ -184,6 +186,8 @@ private:
 
     GRenderWindow* render_window;
 
+    GameListPlaceholder* game_list_placeholder;
+
     // Status bar elements
     QProgressBar* progress_bar = nullptr;
     QLabel* message_label = nullptr;
diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui
index 86c4e46ed..9b0c512e3 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -60,7 +60,6 @@
     <addaction name="action_Load_File"/>
     <addaction name="action_Install_CIA"/>
     <addaction name="separator"/>
-    <addaction name="action_Select_Game_List_Root"/>
     <addaction name="menu_recent_files"/>
     <addaction name="separator"/>
     <addaction name="action_Exit"/>
diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/citra_qt/multiplayer/host_room.cpp
index 99bd83545..93804b1ee 100644
--- a/src/citra_qt/multiplayer/host_room.cpp
+++ b/src/citra_qt/multiplayer/host_room.cpp
@@ -25,7 +25,7 @@
 HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
                                std::shared_ptr<Core::AnnounceMultiplayerSession> session)
     : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
-      ui(std::make_unique<Ui::HostRoom>()), announce_multiplayer_session(session), game_list(list) {
+      ui(std::make_unique<Ui::HostRoom>()), announce_multiplayer_session(session) {
     ui->setupUi(this);
 
     // set up validation for all of the fields
@@ -35,6 +35,15 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
     ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort));
 
     // Create a proxy to the game list to display the list of preferred games
+    game_list = new QStandardItemModel;
+
+    for (int i = 0; i < list->rowCount(); i++) {
+        auto parent = list->item(i, 0);
+        for (int j = 0; j < parent->rowCount(); j++) {
+            game_list->appendRow(parent->child(j)->clone());
+        }
+    }
+
     proxy = new ComboBoxProxyModel;
     proxy->setSourceModel(game_list);
     proxy->sort(0, Qt::AscendingOrder);
@@ -152,8 +161,7 @@ QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const {
 }
 
 bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
-    // TODO(jroweboy): Sort by game title not filename
-    auto leftData = left.data(Qt::DisplayRole).toString();
-    auto rightData = right.data(Qt::DisplayRole).toString();
+    auto leftData = left.data(GameListItemPath::TitleRole).toString();
+    auto rightData = right.data(GameListItemPath::TitleRole).toString();
     return leftData.compare(rightData) < 0;
 }
diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h
index 0b084eab6..d8d1d7019 100644
--- a/src/citra_qt/ui_settings.h
+++ b/src/citra_qt/ui_settings.h
@@ -7,6 +7,7 @@
 #include <array>
 #include <vector>
 #include <QByteArray>
+#include <QMetaType>
 #include <QString>
 #include <QStringList>
 
@@ -19,6 +20,18 @@ static const std::array<std::pair<QString, QString>, 2> themes = {
     {std::make_pair(QString("Default"), QString("default")),
      std::make_pair(QString("Dark"), QString("qdarkstyle"))}};
 
+struct GameDir {
+    QString path;
+    bool deep_scan;
+    bool expanded;
+    bool operator==(const GameDir& rhs) const {
+        return path == rhs.path;
+    };
+    bool operator!=(const GameDir& rhs) const {
+        return !operator==(rhs);
+    };
+};
+
 struct Values {
     QByteArray geometry;
     QByteArray state;
@@ -45,8 +58,9 @@ struct Values {
 
     QString roms_path;
     QString symbols_path;
-    QString gamedir;
-    bool gamedir_deepscan;
+    QString game_dir_deprecated;
+    bool game_dir_deprecated_deepscan;
+    QList<UISettings::GameDir> game_dirs;
     QStringList recent_files;
     QString language;
 
@@ -74,3 +88,5 @@ struct Values {
 
 extern Values values;
 } // namespace UISettings
+
+Q_DECLARE_METATYPE(UISettings::GameDir*);