GSoC Progress Update
In my last blog post, I explained how selection mode was implemented in Games. That was one of the first steps to support Collections in Games, as an efficient way to select games to add/remove from collection is crucial for managing collections. In this post I’ll be talking about how “Favorites Collection” will be implemented in GNOME Games.
Collections
The first thing to do was to introduce a Collection
interface to define a behavior that all types of collections must follow.
All collections must have an ID and a title. Apart from that, all collections must provide a way to add and remove games from it. And on adding or removing a game from the collection, it should emit a “game added” or “game removed” signal respectively. A collection must also implement a load()
, which when called, should load the games belonging to a collection from the database. Since there’s going to be different types of collections, how a collection has to be loaded might differ from each other.
Every collection has its own GameModel
and must implement a get_game_model()
. A GameModel
is a ListModel
which stores the list of games in a collection, and get_game_model()
returns its GameModel
which can be bound to the flowbox of a GamesPage
(a widget where games can be displayed with thumbnail and title).
Other than these, all collections must also implement on_game_added()
, on_game_removed()
and on_game_replaced()
. These are unrelated to games being added or removed to or from a collection. These has to do with games being discovered, and when some games are no longer available to the app. When a game is discovered by tracker or a cached game is loaded, it is added to a games
hash table. This emits a game_added
signal (unrelated to a collection’s game_added
), which every collection listens to. If the added game belongs to the collection, it adds this game to the collection. Similarly on_game_removed()
and on_game_replaced()
handles stuff related to when a game which was cached but is no longer found by the app, and when a game has been renamed, moved to a different directory, or when it’s still the same cached game but with different UID etc.
With the general behavior of a collection defined, it was time to introduce a FavoritesCollection
which implements Collection
.
Favorites Collection
As obvious from the name, a “Favorites collection” is a collection that stores games that a user marks as favorite. Favorite games are marked with a star icon on the thumbnail. A game can be added to favorites from the main Games Page or from the Platforms Page. A game can removed from favorites from the above said pages as well as from the Favorites Collection Page. Games are added or removed from favorites “automagically” depending on the list of currently selected games. If all of the selected games are favorite then clicking on the favorite action in the action bar will remove them from favorites. If none of the selected games are favorite, then they are added to favorite. If selected games are a mix of favorite and non favorite games, then all the non favorite games are added to favorites. The icon in the favorite action button in the action bar, dynamically changes from starred to semi-starred to non-starred depending on the selected games. This along with tool tips help users know what the button will do.
Collections: Behind The Scenes
Database
So once a FavoritesCollection
was ready to go, I needed to work on how it will be stored in the database and how it should load those collections.
Favorite games are stored in the database by having a new is_favorite
column in the games
table. games
table stores all games, including “manually added” games, and games found by tracker. So adding or removing any type of game from Favorites
is a matter of updating the is_favorite
column in games
.
However, I can’t just simply add a new column in the games
table creation query. In order to migrate from the old database with no is_favorite
column to the new one with the column, I’ll have to give some extra commands to the database. This shouldn’t be done always, but should depend on the version of the database. As of now, there isn’t a database versioning system in Games, so a simple database migration support was quickly implemented. This migration leverages a .version
file in the data directory. This was used to migrate from old data directory structure to a newer one. However the file is empty and data directory migration (not database migration) only checks if the .version
file exists or not. This is fine for a one time migration support. But having versions in the .version
file might come in handy later on, so I now configured the database migration to write the version number into the .version
so it can be used later on. No .version
file is treated as version 0, empty .version
is treated as version 1, and from version 2+, the .version
file will have the version number in it. So in this use case, the database migration to support favorite games should be applied for all versions less than 2. Luckily, migration in this case only required to drop the games
table and create it again with the new is_favorite
column :). And after applying this migration, bump the database version and write it into the .version
file. Now any future migrations may make use of this .version
file too.
The database allows to add, remove, fetch complete list of favorite games, and check if a game is favorite. That’s about it for Favorites. For other types of collections, which will be introduced in the upcoming days, they would mostly be stored in different tables.
Collection Manager
As of now, the only available collection is Favorites Collection. But as said, soon there will be “Recently Played” and custom collections that users can create and manage. So when a CollectionManager
is introduced, it has to be designed keeping in mind that it has to handle all types of collections. But most of the collection specific behavior can be neatly abstracted.
CollectionManager
handles the creation of all types of collections. It consists of a hash table to store all collections. And as of now, it has a FavoritesCollection
object. Since any actions related to a collection is better to be handled by CollectionManager
it also provides a favorite_action()
which accepts a list of games and depending on it adds or removes them to favorites. Apart from that, on creation of CollectionManager
, it calls load()
on every collection in the hash table, which loads all the games that belongs to that particular collection from the database.
Collection Model
It is a ListModel
that contains all the available collections, which can be bound to the CollectionsPage
’s flowbox. CollectionsPage
(similar to GamesPage
) is a widget which displays available collection to the user with thumbnails and title.
Collections: What You See
With all these put together we get a functional Favorites Collection. Here are some pictures:
As you can see, the notable visual changes here are:
- A new Collections Page which can be navigated to, from the view switcher
- A star on thumbnails of games marked as favorite, in Games Page and Platforms Page
- A collection thumbnail which is generated from the covers of games that belong to that collection
- A Collection (Sub)Page which when clicked opens a page with games that belong to that collection
By the way, you can also see the new game covers with the blurred background that I was experimenting with in the last blog post.
So this is where my progress is at currently. You can see the relevant MR here.
Whats Next?
In the upcoming weeks I’ll be implementing the rest of the collections. Currently my plan is to implement “Recently Played” collection next as it would the simpler one to implement next. There are also some smaller stuff which needs some work such as search and selection support for Collections. But that is planned to be at the end, after introducing all types of collection.
Conclusion
The work is going great and all the challenges have been fun to solve so far. Many thanks to my mentor, Alexander for being very helpful as always. Thank you all for reading my blog posts.
See you next time :)