Merge branch 'favorite' of https://github.com/Adehom/lutris into Adehom-favorite

This commit is contained in:
Mathieu Comandon 2020-07-07 22:46:15 -07:00
commit 02c1667a04
7 changed files with 325 additions and 60 deletions

View file

@ -6,16 +6,16 @@ import os
import signal
from gettext import gettext as _
# Third Party Libraries
from gi.repository import Gio
# Lutris Modules
from lutris import pga
from lutris.command import MonitoredCommand
from lutris.game import Game
from lutris.gui import dialogs
from lutris.gui.config.add_game import AddGameDialog
from lutris.gui.config.edit_game import EditGameConfigDialog
from lutris.gui.config.add_favorite_games import AddFavoriteGamesDialog
from lutris.gui.config.delete_favorite_games import DeleteFavoriteGamesDialog
from lutris.gui.dialogs.log import LogWindow
from lutris.gui.dialogs.uninstall_game import UninstallGameDialog
from lutris.gui.installerwindow import InstallerWindow
@ -24,6 +24,12 @@ from lutris.util import xdgshortcuts
from lutris.util.log import logger
from lutris.util.system import path_exists
def game_in_favorite(game_id):
categories = pga.get_categories_in_game(game_id)
for category in categories :
if category == "favorite":
return True
return False
class GameActions:
@ -65,6 +71,8 @@ class GameActions:
("install", _("Install"), self.on_install_clicked),
("add", _("Add installed game"), self.on_add_manually),
("configure", _("Configure"), self.on_edit_game_configuration),
("favorite", _("Add to favorites"), self.on_add_favorite_game),
("deletefavorite", _("Remove from favorites"), self.on_delete_favorite_game),
("execute-script", _("Execute script"), self.on_execute_script_clicked),
("browse", _("Browse files"), self.on_browse_files),
(
@ -123,40 +131,38 @@ class GameActions:
def get_displayed_entries(self):
"""Return a dictionary of actions that should be shown for a game"""
return {
"add":
not self.game.is_installed and not self.game.is_search_result,
"install":
not self.game.is_installed,
"play":
self.game.is_installed and not self.is_game_running,
"stop":
self.is_game_running,
"show_logs":
self.game.is_installed,
"configure":
bool(self.game.is_installed),
"install_more":
self.game.is_installed and not self.game.is_search_result,
"execute-script":
bool(self.game.is_installed and self.game.runner.system_config.get("manual_command")),
"desktop-shortcut":
(self.game.is_installed and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)),
"menu-shortcut":
(self.game.is_installed and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)),
"rm-desktop-shortcut":
bool(self.game.is_installed and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)),
"rm-menu-shortcut":
bool(self.game.is_installed and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)),
"browse":
self.game.is_installed and self.game.runner_name != "browser",
"remove":
not self.game.is_search_result,
"view":
True,
"hide":
not GameActions.is_game_hidden(self.game),
"unhide":
GameActions.is_game_hidden(self.game)
"add": not self.game.is_installed and not self.game.is_search_result,
"install": not self.game.is_installed,
"play": self.game.is_installed and not self.is_game_running,
"stop": self.is_game_running,
"show_logs": self.game.is_installed,
"configure": bool(self.game.is_installed),
"favorite": self.game.is_installed and bool(not game_in_favorite(self.game_id)),
"deletefavorite": self.game.is_installed and bool(game_in_favorite(self.game_id)
),
"install_more": self.game.is_installed and not self.game.is_search_result,
"execute-script": bool(self.game.is_installed and self.game.runner.system_config.get("manual_command")),
"desktop-shortcut": (
self.game.is_installed
and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
),
"menu-shortcut": (
self.game.is_installed
and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
),
"rm-desktop-shortcut": bool(
self.game.is_installed
and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
),
"rm-menu-shortcut": bool(
self.game.is_installed
and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
),
"browse": self.game.is_installed and self.game.runner_name != "browser",
"remove": not self.game.is_search_result,
"view": True,
"hide": not GameActions.is_game_hidden(self.game),
"unhide": GameActions.is_game_hidden(self.game)
}
def on_game_run(self, *_args):
@ -207,6 +213,14 @@ class GameActions:
"""Edit game preferences"""
EditGameConfigDialog(self.window, self.game)
def on_add_favorite_game(self, _widget):
"""Add to favorite Games list"""
AddFavoriteGamesDialog(self.window, self.game)
def on_delete_favorite_game(self, _widget):
"""delete to favorite Games list"""
DeleteFavoriteGamesDialog(self.window, self.game)
def on_execute_script_clicked(self, _widget):
"""Execute the game's associated script"""
manual_command = self.game.runner.system_config.get("manual_command")

View file

@ -0,0 +1,68 @@
import re
from gi.repository import Gtk, Pango
from lutris import pga
from lutris.gui.dialogs import Dialog
from lutris.gui.config.common import GameDialogCommon
class AddFavoriteGamesDialog(Dialog, GameDialogCommon):
"""Game category edit dialog."""
def __init__(self, parent, game):
super().__init__("Favorite Games")
self.parent = parent
self.game = game
self.game_id = game.id
self.grid = Gtk.Grid()
self.set_default_size(350, 150)
self.set_border_width(10)
favorite_entry_label = Gtk.Label(
"Do you want to add %s in your favorite games list ? "% game.name, parent=parent
)
favorite_entry_label.set_max_width_chars(80)
favorite_entry_label.set_property("wrap", True)
self.vbox.add(favorite_entry_label)
self.vbox.set_homogeneous(False)
self.vbox.set_spacing(10)
self.vbox.pack_start(self._create_add_favorite(), False, False, 0)
self.build_action_area(self.on_save)
self.show_all()
def _create_add_favorite(self):
def on_add_favorite(widget=None):
pga.add_category_favorite()
pga.add_game_to_category(self.game_id, "favorite")
self.parent.on_game_updated(self.game)
self.destroy()
hbox = Gtk.HBox()
hbox.set_spacing(10)
button = Gtk.Button.new_with_label("Add to Favorite Games")
button.connect("clicked", on_add_favorite)
button.set_tooltip_text("Adds the Games to the Favorite Games list.")
hbox.pack_start(button, False, False, 0)
return hbox
# Override the save action box, because we don't need the advanced-checkbox
def build_action_area(self, button_callback, callback2=None):
self.action_area.set_layout(Gtk.ButtonBoxStyle.END)
# Buttons
hbox = Gtk.Box()
cancel_button = Gtk.Button(label="Cancel")
cancel_button.connect("clicked", self.on_cancel_clicked)
hbox.pack_start(cancel_button, True, True, 10)
save_button = Gtk.Button(label="Save")
if callback2:
save_button.connect("clicked", button_callback, callback2)
else:
save_button.connect("clicked", button_callback)
self.action_area.pack_start(hbox, True, True, 0)

View file

@ -0,0 +1,67 @@
import re
from gi.repository import Gtk, Pango
from lutris import pga
from lutris.gui.dialogs import Dialog
from lutris.gui.config.common import GameDialogCommon
class DeleteFavoriteGamesDialog(Dialog, GameDialogCommon):
"""Game category edit dialog."""
def __init__(self, parent, game):
super().__init__("Favorite Games")
self.parent = parent
self.game = game
self.game_id = game.id
self.grid = Gtk.Grid()
self.set_default_size(350, 150)
self.set_border_width(10)
favorite_entry_label = Gtk.Label(
"Do you want to delete %s in your favorite games list ? "% game.name, parent=parent
)
favorite_entry_label.set_max_width_chars(80)
favorite_entry_label.set_property("wrap", True)
self.vbox.add(favorite_entry_label)
self.vbox.set_homogeneous(False)
self.vbox.set_spacing(10)
self.vbox.pack_start(self._create_delete_favorite(), False, False, 0)
self.build_action_area(self.on_save)
self.show_all()
def _create_delete_favorite(self):
def on_delete_favorite(widget=None):
pga.delete_game_by_id_from_category(self.game_id, "favorite")
self.parent.on_game_updated(self.game)
self.destroy()
hbox = Gtk.HBox()
hbox.set_spacing(10)
button = Gtk.Button.new_with_label("Delete to Favorite Games")
button.connect("clicked", on_delete_favorite)
button.set_tooltip_text("Delete the Games to the Favorite Games list.")
hbox.pack_start(button, False, False, 0)
return hbox
# Override the save action box, because we don't need the advanced-checkbox
def build_action_area(self, button_callback, callback2=None):
self.action_area.set_layout(Gtk.ButtonBoxStyle.END)
# Buttons
hbox = Gtk.Box()
cancel_button = Gtk.Button(label="Cancel")
cancel_button.connect("clicked", self.on_cancel_clicked)
hbox.pack_start(cancel_button, True, True, 10)
save_button = Gtk.Button(label="Save")
if callback2:
save_button.connect("clicked", button_callback, callback2)
else:
save_button.connect("clicked", button_callback)
self.action_area.pack_start(hbox, True, True, 0)

View file

@ -86,6 +86,7 @@ class LutrisWindow(Gtk.ApplicationWindow): # pylint: disable=too-many-public-me
self.threads_stoppers = []
self.selected_runner = None
self.selected_platform = None
self.selected_category = None
self.icon_type = None
# Load settings
@ -530,7 +531,7 @@ class LutrisWindow(Gtk.ApplicationWindow): # pylint: disable=too-many-public-me
child = scrollwindow_children[0]
child.destroy()
self.games_scrollwindow.add(self.view)
self.set_selected_filter(self.selected_runner, self.selected_platform)
self.set_selected_filter(self.selected_runner, self.selected_platform, self.selected_category)
self.set_show_installed_state(self.filter_installed)
self.view.show_all()
@ -892,18 +893,23 @@ class LutrisWindow(Gtk.ApplicationWindow): # pylint: disable=too-many-public-me
def on_sidebar_changed(self, widget):
row = widget.get_selected_row()
if row is None:
self.set_selected_filter(None, None)
self.set_selected_filter(None, None, None)
elif row.type == "runner":
self.set_selected_filter(row.id, None)
self.set_selected_filter(row.id, None, None)
elif row.type == "favorite":
self.set_selected_filter(None, None, row.id)
else:
self.set_selected_filter(None, row.id)
self.set_selected_filter(None, row.id, None)
def set_selected_filter(self, runner, platform):
def set_selected_filter(self, runner, platform, category):
"""Filter the view to a given runner and platform"""
self.selected_runner = runner
self.selected_platform = platform
self.selected_category = category
self.game_store.filter_runner = self.selected_runner
self.game_store.filter_platform = self.selected_platform
self.game_store.filter_category = self.selected_category
self.invalidate_game_filter()
def show_invalid_credential_warning(self):

View file

@ -105,6 +105,7 @@ class GameStore(GObject.Object):
self.filter_text = None
self.filter_runner = None
self.filter_platform = None
self.filter_category = None
self.store = Gtk.ListStore(
int,
str,
@ -226,6 +227,10 @@ class GameStore(GObject.Object):
platform = model.get_value(_iter, COL_PLATFORM)
if platform != self.filter_platform:
return False
if self.filter_category:
identifier = model.get_value(_iter, COL_ID)
if (identifier is None) or identifier not in pga.get_games_in_categories(self.filter_category):
return False
return True
def sort_view(self, key="name", ascending=True):

View file

@ -133,6 +133,7 @@ class SidebarListBox(Gtk.ListBox):
self.active_platforms = pga.get_used_platforms()
self.runners = sorted(runners.__all__)
self.platforms = sorted(platforms.__all__)
self.sidebar_categories = dict() # We have to keep track on the elements somewhere
GObject.add_emission_hook(RunnersDialog, "runner-installed", self.update)
GObject.add_emission_hook(RunnersDialog, "runner-removed", self.update)
@ -160,6 +161,9 @@ class SidebarListBox(Gtk.ListBox):
icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
self.add(SidebarRow(platform, "platform", platform, icon))
self.add(SidebarRow(None, "category", "All", None))
self.add(SidebarRow("category", "favorite", "Favorite Games", None))
self.set_filter_func(self._filter_func)
self.set_header_func(self._header_func)
self.update()
@ -172,23 +176,45 @@ class SidebarListBox(Gtk.ListBox):
if row.id is None:
return True # 'All'
return row.id in self.installed_runners
if len(self.active_platforms) <= 1:
elif row.type == "favorite":
if len(self.sidebar_categories) < 1:
return False # Hide useless filter
return True
elif len(self.active_platforms) <= 1:
return False # Hide useless filter
if row.id is None: # 'All'
elif row.id is None: # 'All'
return True
return row.id in self.active_platforms
def _header_func(self, row, before):
if row.get_header():
return
if not before:
row.set_header(SidebarHeader(_("Runners")))
elif before.type == "runner" and row.type == "platform":
row.set_header(SidebarHeader(_("Platforms")))
elif before.type == "platform" and row.type == "category":
row.set_header(SidebarHeader("Category"))
def add_category_entries(self):
categories = pga.get_categories()
for category in pga.get_categories():
if category not in self.sidebar_categories.keys():
temp = SidebarRow(category, "Favorite Games", category, None)
self.sidebar_categories[category] = temp
self.add(temp)
removalbe_categories = []
for sidebar_category in self.sidebar_categories.keys():
if sidebar_category not in categories:
self.remove(self.sidebar_categories[sidebar_category])
removalbe_categories.append(sidebar_category)
for rem_category in removalbe_categories:
del self.sidebar_categories[rem_category]
self.show_all()
def update(self, *args): # pylint: disable=unused-argument
self.installed_runners = [runner.name for runner in runners.get_installed()]
self.active_platforms = pga.get_used_platforms()
self.add_category_entries()
self.invalidate_filter()
return True

View file

@ -142,15 +142,20 @@ DATABASE = {
},
],
"sources": [
{
"name": "id",
"type": "INTEGER",
"indexed": True
},
{
"name": "uri",
"type": "TEXT UNIQUE"
},
{"name": "id", "type": "INTEGER", "indexed": True},
{"name": "uri", "type": "TEXT UNIQUE"},
],
"categories": [
{"name": "id", "type": "INTEGER", "indexed": True},
{"name": "category", "type": "TEXT"},
{"name": "category", "type": "UNIQUE"},
],
"games_categories": [
{"name": "game_id", "type": "INTEGER", "indexed": False},
{"name": "category_id", "type": "INTEGER", "indexed": False},
{"name": "games", "type": "REFERENCE", "indexed": False, "referenced": "id"},
{"name": "categories", "type": "REFERENCE", "indexed": False, "referenced": "id"},
#{"name": "games, categories", "type": "UNIQUE"},
]
}
@ -182,10 +187,15 @@ def get_schema(tablename):
def field_to_string(name="", type="", indexed=False): # pylint: disable=redefined-builtin
"""Converts a python based table definition to it's SQL statement"""
field_query = "%s %s" % (name, type)
if indexed:
field_query += " PRIMARY KEY"
return field_query
if type == "UNIQUE":
return "UNIQUE (%s)" % name
if referenced is None:
field_query = "%s %s" % (name, type)
if indexed:
field_query += " PRIMARY KEY"
return field_query
return "FOREIGN KEY (%s) REFERENCES %s(%s)" % (name, name, referenced)
def create_table(name, schema):
@ -310,7 +320,7 @@ def get_games_where(**conditions):
if condition:
query = " WHERE ".join((query, condition))
else:
# FIXME: Inspect and document why we should return
# Inspect and document why we should return
# an empty list when no condition is present.
return []
return sql.db_query(PGA_DB, query, tuple(condition_values))
@ -412,7 +422,6 @@ def delete_game(game_id):
"""Delete a game from the PGA."""
sql.db_delete(PGA_DB, "games", "id", game_id)
def set_uninstalled(game_id):
sql.db_update(PGA_DB, "games", {"installed": 0, "runner": ""}, ("id", game_id))
@ -510,7 +519,6 @@ def get_used_platforms_game_count():
results = rows.fetchall()
return {result[0]: result[1] for result in results if result[0]}
def get_hidden_ids():
"""Return a list of game IDs to be excluded from the library view"""
# Load the ignore string and filter out empty strings to prevent issues
@ -525,3 +533,74 @@ def set_hidden_ids(games):
"""Writes a list of game IDs that are to be hidden into the config file"""
ignores_str = [str(game_id) for game_id in games]
settings.write_setting("library_ignores", ','.join(ignores_str), section="lutris")
def get_categories(select="*"):
"""Get the list of every category in database."""
category_result = sql.db_select(PGA_DB, "categories",)
if category_result:
#print (category_result[0])
return category_result[0]
return {}
#query = "select %s from categories" % (select)
#params = []
#query += " ORDER BY category"
#return_categories = []
#for category in sql.db_query(PGA_DB, query, tuple(params)):
# return_categories.append(category["category"])
#print(return_categories)
#return return_categories
def get_games_in_categories(category="*"):
"""Get the ids of games in database."""
query = "select games.id from games " \
"JOIN games_categories ON games.id = games_categories.games " \
"JOIN categories ON categories.id = games_categories.categories " \
"WHERE categories.category = ?" % (category)
params = []
return [
game["id"]
for game in sql.db_query(PGA_DB, query, tuple(params)):
]
return return_ids
def get_categories_in_game(game_id=-1):
"""Get the categories of a game in database."""
if game_id < 0:
return []
query = (
"select categories.category from categories "
"JOIN games_categories ON categories.id = games_categories.category_id "
"JOIN games ON games.id = games_categories.game_id "
"WHERE games.id = \"?\""
)
return [
category["category"]
for category in sql.db_query(PGA_DB, query, (game_id,)):
]
def add_category_favorite():
"""Add a favorite category to the PGA database if it doesn't exist"""
try:
return sql.db_insert(PGA_DB, "categories", {"category": "favorite"})
except Exception as e:
print("")
def add_game_to_category(game_id=-1, category=None):
"""Add a m2m reference from game2category to the PGA database."""
query = "insert into games_categories (games, categories) " \
"select " + str(game_id) + " as games, categories.id as categories from categories " \
"where categories.category = \"" + category + "\""
with sql.db_cursor(PGA_DB) as cursor:
sql.cursor_execute(cursor, query)
def delete_game_by_id_from_category(game_id, category):
"""Delete a game to category entry from the m2m table."""
query = "DELETE FROM games_categories WHERE categories IN " \
"( SELECT games_categories.categories from categories WHERE games_categories.categories = categories.id " \
"AND categories.category = \"" + category + "\" ) AND games_categories.games = " + str(game_id)
with sql.db_cursor(PGA_DB) as cursor:
sql.cursor_execute(cursor, query)