mirror of
https://github.com/lutris/lutris
synced 2024-10-02 22:14:23 +00:00
Smarter searching in the LutrisWindow.
This PR no longer uses SQL to search for games, but instead does it in Python. However, the benefit is a more forgiving search. The game names and search text are normalized to form KD, and combining characters are extra whitespace are stripped. This means that "The Signal From Tölva" can now be found by searching for "from Tolva". This is a bit sloppy, and a bit "Do what I mean"- but even languages which consider 'ö' a distinct letter should be okay, since they surely won't have games that differ in their names only in that one letter. At least, not often.
This commit is contained in:
parent
22b0dac784
commit
71765daf4f
|
@ -39,7 +39,7 @@ from lutris.util.jobs import AsyncCall
|
|||
from lutris.util.library_sync import LOCAL_LIBRARY_UPDATED, sync_local_library
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.path_cache import MISSING_GAMES, add_to_path_cache
|
||||
from lutris.util.strings import get_natural_sort_key
|
||||
from lutris.util.strings import get_natural_sort_key, strip_accents
|
||||
from lutris.util.system import update_desktop_icons
|
||||
|
||||
|
||||
|
@ -407,17 +407,20 @@ class LutrisWindow(Gtk.ApplicationWindow, DialogLaunchUIDelegate, DialogInstallU
|
|||
|
||||
def get_recent_games(self):
|
||||
"""Return a list of currently running games"""
|
||||
searches, _filters, excludes = self.get_sql_filters()
|
||||
games = games_db.get_games(searches=searches, filters={"installed": "1"}, excludes=excludes)
|
||||
games = games_db.get_games(filters={"installed": "1"})
|
||||
games = [game for game in games if self.game_matches(game)]
|
||||
return sorted(games, key=lambda game: max(game["installed_at"] or 0, game["lastplayed"] or 0), reverse=True)
|
||||
|
||||
def game_matches(self, game):
|
||||
if self.filters.get("installed"):
|
||||
if game["appid"] not in games_db.get_service_games(self.service.id):
|
||||
return False
|
||||
if not self.filters.get("text"):
|
||||
text = self.filters.get("text")
|
||||
if not text:
|
||||
return True
|
||||
return self.filters["text"] in game["name"].lower()
|
||||
text = strip_accents(text).casefold()
|
||||
name = strip_accents(game["name"]).casefold()
|
||||
return text in name
|
||||
|
||||
def set_service(self, service_name):
|
||||
if self.service and self.service.id == service_name:
|
||||
|
@ -469,26 +472,24 @@ class LutrisWindow(Gtk.ApplicationWindow, DialogLaunchUIDelegate, DialogInstallU
|
|||
excluded = [".hidden"] if category != ".hidden" else []
|
||||
category_game_ids = categories_db.get_game_ids_for_categories(included, excluded)
|
||||
|
||||
searches, filters, excludes = self.get_sql_filters()
|
||||
games = games_db.get_games(searches=searches, filters=filters, excludes=excludes)
|
||||
games = [game for game in games if game["id"] in category_game_ids]
|
||||
filters = self.get_sql_filters()
|
||||
games = games_db.get_games(filters=filters)
|
||||
games = [game for game in games if game["id"] in category_game_ids and self.game_matches(game)]
|
||||
return self.apply_view_sort(games)
|
||||
|
||||
def get_sql_filters(self):
|
||||
"""Return the current filters for the view"""
|
||||
sql_filters = {}
|
||||
sql_excludes = {}
|
||||
if self.filters.get("runner"):
|
||||
sql_filters["runner"] = self.filters["runner"]
|
||||
if self.filters.get("platform"):
|
||||
sql_filters["platform"] = self.filters["platform"]
|
||||
if self.filters.get("installed"):
|
||||
sql_filters["installed"] = "1"
|
||||
if self.filters.get("text"):
|
||||
searches = {"name": self.filters["text"]}
|
||||
else:
|
||||
searches = None
|
||||
return searches, sql_filters, sql_excludes
|
||||
|
||||
# We omit the "text" search here because SQLite does a fairly literal
|
||||
# search, which is accent sensitive. We'll do better with self.game_matches()
|
||||
return sql_filters
|
||||
|
||||
def get_service_media(self, icon_type):
|
||||
"""Return the ServiceMedia class used for this view"""
|
||||
|
|
|
@ -42,6 +42,38 @@ def slugify(value: str) -> str:
|
|||
return slug
|
||||
|
||||
|
||||
def strip_accents(value: str) -> str:
|
||||
"""Returns a string that is 'value', but with combining characters removed.
|
||||
|
||||
This normalizes the text to form KD, which also removes any compatibility characters,
|
||||
so things like ellipsis are expanded to '...'.
|
||||
|
||||
This also strips leading a trailing whitespace, and normalizes all remaining whitespace
|
||||
to single spaces.
|
||||
|
||||
This does allow non-ASCII characters (like Greek or Cyrillic), and does not interfere with
|
||||
casing.
|
||||
|
||||
We use this for a more forgiving search.
|
||||
"""
|
||||
if value:
|
||||
decomposed = unicodedata.normalize("NFKD", value)
|
||||
result = ""
|
||||
prev_whitespace = False
|
||||
for ch in reversed(decomposed.strip()):
|
||||
combining = unicodedata.combining(ch)
|
||||
if not combining:
|
||||
whitespace = ch.isspace()
|
||||
if whitespace:
|
||||
if not prev_whitespace:
|
||||
result += " "
|
||||
else:
|
||||
result += ch
|
||||
prev_whitespace = whitespace
|
||||
return result[::-1] # We built the text backwards, so we must reverse it
|
||||
return value
|
||||
|
||||
|
||||
def get_natural_sort_key(value: str, number_width: int = 16) -> str:
|
||||
"""Returns a string with the numerical parts (runs of digits)
|
||||
0-padded out to 'number_width' digits."""
|
||||
|
|
Loading…
Reference in a new issue