From 886fdb2687aee2b5aba2c9c53bd73400cde71741 Mon Sep 17 00:00:00 2001
From: Kewlan <colin_rehn@hotmail.com>
Date: Sat, 10 Apr 2021 11:09:01 +0200
Subject: [PATCH] game_list: Mark games as favorite to make them appear at the
 top. Icons are from Icons8.

---
 dist/license.md                               |   3 +
 dist/qt_themes/colorful/icons/48x48/star.png  | Bin 0 -> 1248 bytes
 dist/qt_themes/colorful/style.qrc             |   1 +
 dist/qt_themes/default/default.qrc            |   1 +
 dist/qt_themes/default/icons/48x48/star.png   | Bin 0 -> 686 bytes
 .../qt_themes/qdarkstyle/icons/48x48/star.png | Bin 0 -> 725 bytes
 dist/qt_themes/qdarkstyle/style.qrc           |   1 +
 .../icons/48x48/star.png                      | Bin 0 -> 725 bytes
 .../qdarkstyle_midnight_blue/style.qrc        |   1 +
 license.txt                                   |   1 +
 src/yuzu/configuration/config.cpp             |  14 +++
 src/yuzu/game_list.cpp                        | 103 +++++++++++++++++-
 src/yuzu/game_list.h                          |   5 +
 src/yuzu/game_list_p.h                        |  26 ++++-
 src/yuzu/uisettings.h                         |   1 +
 15 files changed, 152 insertions(+), 5 deletions(-)
 create mode 100644 dist/qt_themes/colorful/icons/48x48/star.png
 create mode 100644 dist/qt_themes/default/icons/48x48/star.png
 create mode 100644 dist/qt_themes/qdarkstyle/icons/48x48/star.png
 create mode 100644 dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png

diff --git a/dist/license.md b/dist/license.md
index e9bc87656d..7bdebfec1f 100644
--- a/dist/license.md
+++ b/dist/license.md
@@ -12,6 +12,7 @@ qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
 qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/qdarkstyle/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io
 qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
@@ -20,6 +21,7 @@ qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
 qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/qdarkstyle/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/colorful/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io
 qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
@@ -28,5 +30,6 @@ qt_themes/colorful/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/colorful/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/colorful/icons/48x48/plus.png | CC BY-ND 3.0 | https://icons8.com
 qt_themes/colorful/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
+qt_themes/colorful/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
 
 <!-- TODO: Add the license of the yuzu icon -->
\ No newline at end of file
diff --git a/dist/qt_themes/colorful/icons/48x48/star.png b/dist/qt_themes/colorful/icons/48x48/star.png
new file mode 100644
index 0000000000000000000000000000000000000000..43b5d52ed7cca57aec41d75670cf29cfa98e849d
GIT binary patch
literal 1248
zcmV<61Rwi}P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00006VoOIv0RI60
z0RN!9r;`8x1eHldK~!jg?U`+eRAm&$f9KBX8ZJaw>8gt_A)#g&J3Av1fne{HUlM%5
zO~jxG(WMVzp9(7c6xkOEDlCE^iJ$~4g2v4VT|sl*)>2qY&Fw|fwB4QAxzE23>$)?u
zGk4~W_u7Z{z%ZOU&vVZI|D6AGp64<!VG}lCv6;FzCDpwl8D|cQE3kUor{U~02t4N6
z!uK!4k;jW8C;+l&K}|?a<o##jN)t;3baG20*-L$(W)Ps?m^x{$&D%W`b*^|Sko#+0
z0eFz6b2+yrj#Tjw5L>}<75vDzx#y+mD;_xkb+3OAbWaiZk<VQmU%N8ue9>3H=s@GZ
zkNC=WM4v7~Q_tGPIO_)42yjY>Aj~bbB_AJ)DrdA6aMm#fzHAi?r!7&YjI09se#7l}
z`JX{k#=t`{831KzNX)gxd-|iu8(jqw`R71WD(ZjCZzMMy*})b`1=P233RebCBc>Mv
zhw{GrgMr35wtUav_2#aNfO^}fk;^t?v;Z^kxF4sDSAV6lgbJQXyD@d*te=EC!_egO
zmi{F4eQ6v4o!BrB-)3RnY@C|GwSZ=TW`oT{Zq_xOLur?(1h#lgiLWt#2sj7&RgSZm
z-|yj_!Tj?$JB#m~v-FpJ6$z+k<Ex+-%dTLcBr5sUW2jn;62d||63NW=QOpg+%dg{@
ze56F+iUxO?DpeR8qdH=E(wRcY<0aJ7z6#ujNH>+TFY8~QWWvPo&9udm`*#Hr0MHNX
zm*KP*G3~}nrfi<9$+cUm^P^I%6}ymn)-S@j&p|E!rDQ-CO<HBC&dzc;1KX%hteHtp
z`#^V>mqP_vMI#wN=S}vgr7}k=VXS%%=-9eju;EXU?z@4K0n}^eKWxbZzXai{=^)d|
zM;aNJ`T^+48z32gLuU4vB{P3k!&cK>$QJC&@}sT6+*{+zJ>Hj8BAdc7uI!uw|1+~I
zD{27`bzCKMbkz&Mj=CiiAUjF#dH;BuZAB(uE1vr9LPwr>1GF_l$sqKum+ahZc5OgS
z)=+&&88bB%B@gT}wRI|_Y*_I16OhtTNT$#?Q>q>);rBq#>Lzjn7eR^gk_qTc!FQ@*
z%e$Lx`FEk-5yHQb<pz_|DEP{qs3yPo*(4gcH!Pg+1e~7=1XBH#vN9P*Xc>jx12R(M
zfl!-Yr8BuY_CXz+SgAU@OtD`kh7q1XC<*ECL(Ryw)F_#LLz~U?G}7-?^gsG+z7DtE
z8XEx^Yo7sH9+vc``9pVFTjx8L{N`<&nz%#aWl(lxj;ln*7`C?92sl$)74Un_5(^Wl
z{abC>!M{i6+2YKVMEbx>=KLj~y+JV?E*%C_s6AjPUEyHrHP1S%HS<N_{ACAE0*|Y!
zZKZtM30hPT0}zJdseptbwU(^d1#@nj_1(6_TE7kiUqqy>>nm&Uml|wCx>Qn$v_kxk
zQ1d_?d0>Hj|EH@<m6O=%`2(*MN~Iptp|%;$AAZfSr;t1lme71lAM2|=JB)+|y$ejv
z9|fH<W4o8koT?jjfmy1nAK0XCm+lgJ9W;l;q{~snp0Ek~KiI#Dl}YOhO?A5f0000<
KMNUMnLSTa5I$LP~

literal 0
HcmV?d00001

diff --git a/dist/qt_themes/colorful/style.qrc b/dist/qt_themes/colorful/style.qrc
index 36735519a5..18b10869eb 100644
--- a/dist/qt_themes/colorful/style.qrc
+++ b/dist/qt_themes/colorful/style.qrc
@@ -7,6 +7,7 @@
         <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="48x48/star.png">icons/48x48/star.png</file>
         <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
     </qresource>
     <qresource prefix="colorful">
diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc
index 2182f33f3a..b195747a37 100644
--- a/dist/qt_themes/default/default.qrc
+++ b/dist/qt_themes/default/default.qrc
@@ -10,6 +10,7 @@
         <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="48x48/star.png">icons/48x48/star.png</file>
         <file alias="256x256/yuzu.png">icons/256x256/yuzu.png</file>
         <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
     </qresource>
diff --git a/dist/qt_themes/default/icons/48x48/star.png b/dist/qt_themes/default/icons/48x48/star.png
new file mode 100644
index 0000000000000000000000000000000000000000..740f7f3e75da9160a8db6a1941b159509a7ab8ee
GIT binary patch
literal 686
zcmV;f0#W^mP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00006VoOIv0RI60
z0RN!9r;`8x0%J);K~!jg?b$nwOi>uX@gHlmi;094*=0#KY(pZ%dK9QUqM}k03ZhVI
zH6l^bNj#$05i~lL3h}5Ul3ms#5)ui!8<yhS8?%p@x%Zm0cUI<~e8ptu`_BKn_spDg
z?$D-9o0tqGF)!)Cw<LOE4YDv+VVp)KiBl2Bi9#1f@jHnR=q}YLC&&GjPvcmrMmc$E
zW&_W$piHT(957Qpjr}EhW#)m}4csr$D-)YAsdfXC*j$=j2Cl0upT@P)>>98NKQwM&
z486@7w3!Rp8QFg8yR_iK)-5bnjjnzgpRl+Y19ue6>%n?#!ye)4@6M6G6l<^oeZmF!
zB}~Cw%(k!$B*pua;%kLD+mF@Bd$G0!V|XWQL*5E^bp-E|*YEJAUV#g;PlK}JVzuz}
zFYycER<<%DjXrP~-<5x*r^ioWi=K&H_}l{0)oaJmtnw)vG28<3U$8f?e9CG(D^Y$F
zTZ+o3EXSh~<X>Ukzsjd{;r1NLKgP;t$)|MS>Kw@5!IEanS930+{4FesDW7sVg8U5y
z&df!()0KjG^%xf#$wkoyb{5R5$Bqa!XQd)6>&>1*6AGLWirj%A#m^D-=)~5%Nktln
zd8>2Rp7#wDA?60}b!_>*95U~50w?h?W6q=qTZn3oWRRG^6)daDtKht_uWyupC}snV
z&dhg(rxmprOg>Yu#`#F@;9-qHU*TYmI{WalMy-2I^qs;2;dE3z*k_zoY+dL~a=*?}
zGs^!6gH?l!<C3ZKyePf6DLf5H>kl-c?hIDoiEyyl5TX8d;p8_g@-I=FHvbcU0IaAS
UE=gn{XaE2J07*qoM6N<$f)bHM>Hq)$

literal 0
HcmV?d00001

diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/star.png b/dist/qt_themes/qdarkstyle/icons/48x48/star.png
new file mode 100644
index 0000000000000000000000000000000000000000..90d423a1d4c1e05ccec0a01fa34abca9fe99676d
GIT binary patch
literal 725
zcmV;`0xJE9P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00006VoOIv0RI60
z0RN!9r;`8x0*XmQK~!jg?U_Aj6hRn<-!X=Zawr;q&KQOG(<q3-`BSCx2P>_t6%-62
zVr82q2v*vNjbd-7sEw6YRw`;*Au;C>A_fF=QIp5w_Hek|?d?ovvLg4u7RTQAeeXLn
zyE7jvb?MS28bBE+=dHsFpda|2T>Y`?tcsOb(ukyDa$SyCCp<s_sPLC6(3iKa%m5cy
z%K5x?Wd?ZL!oYjL<gKkEz{wWk8=S~nUq^uFDF&Y9tuGZ|Kd_i$U=cVFvS#Sl=ygeH
z>)c4X8n|W{fGxmW%IG(!0i{rNL(aezNm<_Y1^sIq8_-96v&zaeuqH(9)t-X;C2f<m
zLsCi7u%xXHDK<;G3%rz6l{6{oi!tV_tCl4X955tlyQIM;k%N*3l6$d^1vN=ik}8ru
z%QBbzo-)RKY8wrVIV^*`xM^!{euzIvS_n9zF_=uf3!DLd<UPzS_zjF?vIralW^xv6
zQv=R9%@eQ>n8;Z;z&BvnNxWrZ0C<<9_zG~)OT1-b1Mn&b@#DbGWr?>;6o5x7DE>9D
zDTsK>L=SLp1;jrF)&>>dWaLIf@ecv7<Bh9{+Y!Xy$D?QVJx&)r<I<Qn#^4&{Yv8D7
zTpCAwf}E!eC`y`@)Z<eyg<q1^8)N2Nf_!G6tnlI`saMh=*B~DQUQ-<_ch^8*;u-)N
zEoK&~k}gWRBxyQh%q~J7A#8xN88)P!z#U+n6;}jCsh{gRaXO}fPCfGzV0TLV-sC@N
zjK(zZqD|3p;FLq2<G{x@xt_)nZ>V=iHZ#CAz<FN)dXw1M7CFOzM@Xi>!8~vaYn?A6
zZwYt+ELi!6LTGL-v=Mkiy|LL7A^&0Oo!<m8-$Y%y{Ac_D5cb2G%IU<f00000NkvXX
Hu0mjfg8oC0

literal 0
HcmV?d00001

diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc
index 2b91204f3c..34e872d25a 100644
--- a/dist/qt_themes/qdarkstyle/style.qrc
+++ b/dist/qt_themes/qdarkstyle/style.qrc
@@ -8,6 +8,7 @@
     <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="48x48/star.png">icons/48x48/star.png</file>
     <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
   </qresource>
   <qresource prefix="qss_icons">
diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png b/dist/qt_themes/qdarkstyle_midnight_blue/icons/48x48/star.png
new file mode 100644
index 0000000000000000000000000000000000000000..90d423a1d4c1e05ccec0a01fa34abca9fe99676d
GIT binary patch
literal 725
zcmV;`0xJE9P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00006VoOIv0RI60
z0RN!9r;`8x0*XmQK~!jg?U_Aj6hRn<-!X=Zawr;q&KQOG(<q3-`BSCx2P>_t6%-62
zVr82q2v*vNjbd-7sEw6YRw`;*Au;C>A_fF=QIp5w_Hek|?d?ovvLg4u7RTQAeeXLn
zyE7jvb?MS28bBE+=dHsFpda|2T>Y`?tcsOb(ukyDa$SyCCp<s_sPLC6(3iKa%m5cy
z%K5x?Wd?ZL!oYjL<gKkEz{wWk8=S~nUq^uFDF&Y9tuGZ|Kd_i$U=cVFvS#Sl=ygeH
z>)c4X8n|W{fGxmW%IG(!0i{rNL(aezNm<_Y1^sIq8_-96v&zaeuqH(9)t-X;C2f<m
zLsCi7u%xXHDK<;G3%rz6l{6{oi!tV_tCl4X955tlyQIM;k%N*3l6$d^1vN=ik}8ru
z%QBbzo-)RKY8wrVIV^*`xM^!{euzIvS_n9zF_=uf3!DLd<UPzS_zjF?vIralW^xv6
zQv=R9%@eQ>n8;Z;z&BvnNxWrZ0C<<9_zG~)OT1-b1Mn&b@#DbGWr?>;6o5x7DE>9D
zDTsK>L=SLp1;jrF)&>>dWaLIf@ecv7<Bh9{+Y!Xy$D?QVJx&)r<I<Qn#^4&{Yv8D7
zTpCAwf}E!eC`y`@)Z<eyg<q1^8)N2Nf_!G6tnlI`saMh=*B~DQUQ-<_ch^8*;u-)N
zEoK&~k}gWRBxyQh%q~J7A#8xN88)P!z#U+n6;}jCsh{gRaXO}fPCfGzV0TLV-sC@N
zjK(zZqD|3p;FLq2<G{x@xt_)nZ>V=iHZ#CAz<FN)dXw1M7CFOzM@Xi>!8~vaYn?A6
zZwYt+ELi!6LTGL-v=Mkiy|LL7A^&0Oo!<m8-$Y%y{Ac_D5cb2G%IU<f00000NkvXX
Hu0mjfg8oC0

literal 0
HcmV?d00001

diff --git a/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc b/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
index 579e73ece0..142dd3288d 100644
--- a/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
+++ b/dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
@@ -8,6 +8,7 @@
     <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="48x48/star.png">icons/48x48/star.png</file>
     <file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
   </qresource>
   <qresource prefix="qss_icons">
diff --git a/license.txt b/license.txt
index 86e7b3c1b1..495f3e6769 100644
--- a/license.txt
+++ b/license.txt
@@ -358,6 +358,7 @@ chip.png (Colorful, Colorful Dark)          | CC BY-ND 3.0  | https://icons8.com
 folder.png (Colorful, Colorful Dark)        | CC BY-ND 3.0  | https://icons8.com
 plus.png (Colorful, Colorful Dark)          | CC BY-ND 3.0  | https://icons8.com
 sd_card.png (Colorful, Colorful Dark)       | CC BY-ND 3.0  | https://icons8.com
+star.png                                    | CC BY-ND 3.0  | https://icons8.com
 
 Note:
 Some icons are different in different themes, and they are separately listed
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 50ea15e2ab..ff6a6e961e 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -934,6 +934,13 @@ void Config::ReadUIGamelistValues() {
     UISettings::values.row_2_text_id = ReadSetting(QStringLiteral("row_2_text_id"), 2).toUInt();
     UISettings::values.cache_game_list =
         ReadSetting(QStringLiteral("cache_game_list"), true).toBool();
+    const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites"));
+    for (int i = 0; i < favorites_size; i++) {
+        qt_config->setArrayIndex(i);
+        UISettings::values.favorited_ids.append(
+            ReadSetting(QStringLiteral("program_id")).toULongLong());
+    }
+    qt_config->endArray();
 
     qt_config->endGroup();
 }
@@ -1476,6 +1483,13 @@ void Config::SaveUIGamelistValues() {
     WriteSetting(QStringLiteral("row_1_text_id"), UISettings::values.row_1_text_id, 3);
     WriteSetting(QStringLiteral("row_2_text_id"), UISettings::values.row_2_text_id, 2);
     WriteSetting(QStringLiteral("cache_game_list"), UISettings::values.cache_game_list, true);
+    qt_config->beginWriteArray(QStringLiteral("favorites"));
+    for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) {
+        qt_config->setArrayIndex(i);
+        WriteSetting(QStringLiteral("program_id"),
+                     QVariant::fromValue(UISettings::values.favorited_ids[i]));
+    }
+    qt_config->endArray();
 
     qt_config->endGroup();
 }
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index 9afd5b45f5..48b78d65fe 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -11,6 +11,7 @@
 #include <QJsonDocument>
 #include <QJsonObject>
 #include <QKeyEvent>
+#include <QList>
 #include <QMenu>
 #include <QThreadPool>
 #include <fmt/format.h>
@@ -84,6 +85,10 @@ void GameListSearchField::setFilterResult(int visible, int total) {
     label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible));
 }
 
+bool GameListSearchField::isEmpty() const {
+    return edit_filter->text().isEmpty();
+}
+
 QString GameList::GetLastFilterResultItem() const {
     QString file_path;
     const int folder_count = item_model->rowCount();
@@ -187,7 +192,9 @@ void GameList::OnTextChanged(const QString& new_text) {
     // If the searchfield is empty every item is visible
     // Otherwise the filter gets applied
     if (edit_filter_text.isEmpty()) {
-        for (int i = 0; i < folder_count; ++i) {
+        tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+                                UISettings::values.favorited_ids.size() == 0);
+        for (int i = 1; i < folder_count; ++i) {
             folder = item_model->item(i, 0);
             const QModelIndex folder_index = folder->index();
             const int children_count = folder->rowCount();
@@ -198,8 +205,9 @@ void GameList::OnTextChanged(const QString& new_text) {
         }
         search_field->setFilterResult(children_total, children_total);
     } else {
+        tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
         int result_count = 0;
-        for (int i = 0; i < folder_count; ++i) {
+        for (int i = 1; i < folder_count; ++i) {
             folder = item_model->item(i, 0);
             const QModelIndex folder_index = folder->index();
             const int children_count = folder->rowCount();
@@ -280,6 +288,13 @@ void GameList::OnUpdateThemedIcons() {
                     .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
                 Qt::DecorationRole);
             break;
+        case GameListItemType::Favorites:
+            child->setData(
+                QIcon::fromTheme(QStringLiteral("star"))
+                    .pixmap(icon_size)
+                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+                Qt::DecorationRole);
+            break;
         default:
             break;
         }
@@ -427,6 +442,13 @@ void GameList::DonePopulating(const QStringList& watch_list) {
     emit ShowList(!IsEmpty());
 
     item_model->invisibleRootItem()->appendRow(new GameListAddDir());
+    item_model->invisibleRootItem()->insertRow(0, new GameListFavorites());
+    tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+                            UISettings::values.favorited_ids.size() == 0);
+    tree_view->expand(item_model->invisibleRootItem()->child(0)->index());
+    for (const auto id : UISettings::values.favorited_ids) {
+        AddFavorite(id);
+    }
 
     // Clear out the old directories to watch for changes and add the new ones
     auto watch_dirs = watcher->directories();
@@ -446,7 +468,7 @@ void GameList::DonePopulating(const QStringList& watch_list) {
     tree_view->setEnabled(true);
     const int folder_count = tree_view->model()->rowCount();
     int children_total = 0;
-    for (int i = 0; i < folder_count; ++i) {
+    for (int i = 1; i < folder_count; ++i) {
         children_total += item_model->item(i, 0)->rowCount();
     }
     search_field->setFilterResult(children_total, children_total);
@@ -478,6 +500,9 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
     case GameListItemType::SysNandDir:
         AddPermDirPopup(context_menu, selected);
         break;
+    case GameListItemType::Favorites:
+        AddFavoritesPopup(context_menu);
+        break;
     default:
         break;
     }
@@ -485,6 +510,8 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
 }
 
 void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) {
+    QAction* favorite = context_menu.addAction(tr("Favorite"));
+    context_menu.addSeparator();
     QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location"));
     QAction* open_mod_location = context_menu.addAction(tr("Open Mod Data Location"));
     QAction* open_transferable_shader_cache =
@@ -503,6 +530,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
     context_menu.addSeparator();
     QAction* properties = context_menu.addAction(tr("Properties"));
 
+    favorite->setVisible(program_id != 0);
+    favorite->setCheckable(true);
+    favorite->setChecked(UISettings::values.favorited_ids.contains(program_id));
     open_save_location->setVisible(program_id != 0);
     open_mod_location->setVisible(program_id != 0);
     open_transferable_shader_cache->setVisible(program_id != 0);
@@ -513,6 +543,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
     auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
     navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0);
 
+    connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); });
     connect(open_save_location, &QAction::triggered, [this, program_id, path]() {
         emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path);
     });
@@ -576,7 +607,7 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
 
     const int row = selected.row();
 
-    move_up->setEnabled(row > 0);
+    move_up->setEnabled(row > 1);
     move_down->setEnabled(row < item_model->rowCount() - 2);
 
     connect(move_up, &QAction::triggered, [this, selected, row, game_dir_index] {
@@ -614,6 +645,18 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) {
     });
 }
 
+void GameList::AddFavoritesPopup(QMenu& context_menu) {
+    QAction* clear_all = context_menu.addAction(tr("Clear"));
+
+    connect(clear_all, &QAction::triggered, [this] {
+        for (const auto id : UISettings::values.favorited_ids) {
+            RemoveFavorite(id);
+        }
+        UISettings::values.favorited_ids.clear();
+        tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
+    });
+}
+
 void GameList::LoadCompatibilityList() {
     QFile compat_list{QStringLiteral(":compatibility_list/compatibility_list.json")};
 
@@ -728,6 +771,58 @@ void GameList::RefreshGameDirectory() {
     }
 }
 
+void GameList::ToggleFavorite(u64 program_id) {
+    if (!UISettings::values.favorited_ids.contains(program_id)) {
+        tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(),
+                                !search_field->isEmpty());
+        UISettings::values.favorited_ids.append(program_id);
+        AddFavorite(program_id);
+        item_model->sort(tree_view->header()->sortIndicatorSection(),
+                         tree_view->header()->sortIndicatorOrder());
+    } else {
+        UISettings::values.favorited_ids.removeOne(program_id);
+        RemoveFavorite(program_id);
+        if (UISettings::values.favorited_ids.size() == 0) {
+            tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true);
+        }
+    }
+}
+
+void GameList::AddFavorite(u64 program_id) {
+    auto* favorites_row = item_model->item(0);
+
+    for (int i = 1; i < item_model->rowCount() - 1; i++) {
+        const auto* folder = item_model->item(i);
+        for (int j = 0; j < folder->rowCount(); j++) {
+            if (folder->child(j)->data(GameListItemPath::ProgramIdRole).toULongLong() ==
+                program_id) {
+                QList<QStandardItem*> list;
+                for (int k = 0; k < item_model->columnCount(); k++) {
+                    list.append(folder->child(j, k)->clone());
+                }
+                list[0]->setData(folder->child(j)->data(GameListItem::SortRole),
+                                 GameListItem::SortRole);
+                list[0]->setText(folder->child(j)->data(Qt::DisplayRole).toString());
+
+                favorites_row->appendRow(list);
+                return;
+            }
+        }
+    }
+}
+
+void GameList::RemoveFavorite(u64 program_id) {
+    auto* favorites_row = item_model->item(0);
+
+    for (int i = 0; i < favorites_row->rowCount(); i++) {
+        const auto* game = favorites_row->child(i);
+        if (game->data(GameListItemPath::ProgramIdRole).toULongLong() == program_id) {
+            favorites_row->removeRow(i);
+            return;
+        }
+    }
+}
+
 GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} {
     connect(parent, &GMainWindow::UpdateThemedIcons, this,
             &GameListPlaceholder::onUpdateThemedIcons);
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index 58059a3c43..9c0a1a4823 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -112,10 +112,15 @@ private:
 
     void RefreshGameDirectory();
 
+    void ToggleFavorite(u64 program_id);
+    void AddFavorite(u64 program_id);
+    void RemoveFavorite(u64 program_id);
+
     void PopupContextMenu(const QPoint& menu_location);
     void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path);
     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
+    void AddFavoritesPopup(QMenu& context_menu);
 
     std::shared_ptr<FileSys::VfsFilesystem> vfs;
     FileSys::ManualContentProvider* provider;
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h
index f25445f187..7ca8ece231 100644
--- a/src/yuzu/game_list_p.h
+++ b/src/yuzu/game_list_p.h
@@ -29,7 +29,8 @@ enum class GameListItemType {
     SdmcDir = QStandardItem::UserType + 3,
     UserNandDir = QStandardItem::UserType + 4,
     SysNandDir = QStandardItem::UserType + 5,
-    AddDir = QStandardItem::UserType + 6
+    AddDir = QStandardItem::UserType + 6,
+    Favorites = QStandardItem::UserType + 7,
 };
 
 Q_DECLARE_METATYPE(GameListItemType);
@@ -310,6 +311,28 @@ public:
     }
 };
 
+class GameListFavorites : public GameListItem {
+public:
+    explicit GameListFavorites() {
+        setData(type(), TypeRole);
+
+        const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
+        setData(QIcon::fromTheme(QStringLiteral("star"))
+                    .pixmap(icon_size)
+                    .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
+                Qt::DecorationRole);
+        setData(QObject::tr("Favorites"), Qt::DisplayRole);
+    }
+
+    int type() const override {
+        return static_cast<int>(GameListItemType::Favorites);
+    }
+
+    bool operator<(const QStandardItem& other) const override {
+        return false;
+    }
+};
+
 class GameList;
 class QHBoxLayout;
 class QTreeView;
@@ -324,6 +347,7 @@ public:
     explicit GameListSearchField(GameList* parent = nullptr);
 
     void setFilterResult(int visible, int total);
+    bool isEmpty() const;
 
     void clear();
     void setFocus();
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h
index ce3945485f..5ba00b8c80 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -74,6 +74,7 @@ struct Values {
     QString game_dir_deprecated;
     bool game_dir_deprecated_deepscan;
     QVector<UISettings::GameDir> game_dirs;
+    QVector<u64> favorited_ids;
     QStringList recent_files;
     QString language;