Compare commits
27 Commits
gamepad-su
...
yuzu-sourc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
755b733023 | ||
|
|
ad38dc6d49 | ||
|
|
a7efe0a920 | ||
|
|
8d082ab158 | ||
|
|
b3c2437618 | ||
|
|
8e7d08b3b2 | ||
|
|
d22d3820b9 | ||
|
|
721a46c5b8 | ||
|
|
8efb1c6c5e | ||
|
|
07960182c7 | ||
|
|
fccf302c4b | ||
|
|
7a1e5e0968 | ||
|
|
19a1c856ac | ||
|
|
e9b8c0d01e | ||
|
|
95cd65bc7a | ||
|
|
073dbdb97b | ||
|
|
ad0e5b2abf | ||
|
|
9ed085e1a0 | ||
|
|
f4f6d73d4a | ||
|
|
107c1c96b5 | ||
|
|
299333c959 | ||
|
|
e4b315f252 | ||
|
|
455c6891d8 | ||
|
|
2311f3dff7 | ||
|
|
f5b527cc51 | ||
|
|
0a89061218 | ||
|
|
20bfe9309c |
@@ -132,6 +132,15 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Adw.ActionRow {
|
||||||
|
title: _("Import Flatpak Games");
|
||||||
|
activatable-widget: lutris_import_flatpak_switch;
|
||||||
|
|
||||||
|
Switch lutris_import_flatpak_switch {
|
||||||
|
valign: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Adw.ExpanderRow heroic_expander_row {
|
Adw.ExpanderRow heroic_expander_row {
|
||||||
@@ -216,6 +225,29 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Adw.ExpanderRow flatpak_expander_row {
|
||||||
|
title: _("Flatpak");
|
||||||
|
show-enable-switch: true;
|
||||||
|
|
||||||
|
Adw.ActionRow flatpak_data_action_row {
|
||||||
|
title: _("Install Location");
|
||||||
|
|
||||||
|
Button flatpak_data_file_chooser_button {
|
||||||
|
icon-name: "folder-symbolic";
|
||||||
|
valign: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.ActionRow flatpak_import_launchers_row {
|
||||||
|
title: _("Import Game Launchers");
|
||||||
|
activatable-widget: flatpak_import_launchers_switch;
|
||||||
|
|
||||||
|
Switch flatpak_import_launchers_switch {
|
||||||
|
valign: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@
|
|||||||
<key name="lutris-import-steam" type="b">
|
<key name="lutris-import-steam" type="b">
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
</key>
|
</key>
|
||||||
|
<key name="lutris-import-flatpak" type="b">
|
||||||
|
<default>false</default>
|
||||||
|
</key>
|
||||||
<key name="heroic" type="b">
|
<key name="heroic" type="b">
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
</key>
|
</key>
|
||||||
@@ -61,6 +64,15 @@
|
|||||||
<key name="legendary-location" type="s">
|
<key name="legendary-location" type="s">
|
||||||
<default>"~/.config/legendary/"</default>
|
<default>"~/.config/legendary/"</default>
|
||||||
</key>
|
</key>
|
||||||
|
<key name="flatpak" type="b">
|
||||||
|
<default>true</default>
|
||||||
|
</key>
|
||||||
|
<key name="flatpak-location" type="s">
|
||||||
|
<default>"/var/lib/flatpak/exports/"</default>
|
||||||
|
</key>
|
||||||
|
<key name="flatpak-import-launchers" type="b">
|
||||||
|
<default>false</default>
|
||||||
|
</key>
|
||||||
<key name="sgdb-key" type="s">
|
<key name="sgdb-key" type="s">
|
||||||
<default>""</default>
|
<default>""</default>
|
||||||
</key>
|
</key>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"--filesystem=~/.var/app/net.lutris.Lutris/:ro",
|
"--filesystem=~/.var/app/net.lutris.Lutris/:ro",
|
||||||
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
|
"--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro",
|
||||||
"--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro",
|
"--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro",
|
||||||
"--filesystem=~/.var/app/io.itch.itch/config/itch/:ro"
|
"--filesystem=~/.var/app/io.itch.itch/config/itch/:ro",
|
||||||
|
"--filesystem=/var/lib/flatpak:ro"
|
||||||
],
|
],
|
||||||
"cleanup" : [
|
"cleanup" : [
|
||||||
"/include",
|
"/include",
|
||||||
@@ -96,6 +97,20 @@
|
|||||||
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
|
"sha256": "bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "python3-pyxdg",
|
||||||
|
"buildsystem": "simple",
|
||||||
|
"build-commands": [
|
||||||
|
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pyxdg\" --no-build-isolation"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"url": "https://files.pythonhosted.org/packages/e5/8d/cf41b66a8110670e3ad03dab9b759704eeed07fa96e90fdc0357b2ba70e2/pyxdg-0.28-py2.py3-none-any.whl",
|
||||||
|
"sha256": "bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -169,8 +169,7 @@ class DetailsWindow(Adw.Window):
|
|||||||
"hidden": False,
|
"hidden": False,
|
||||||
"source": "imported",
|
"source": "imported",
|
||||||
"added": int(time()),
|
"added": int(time()),
|
||||||
},
|
}
|
||||||
allow_side_effects=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Game(Gtk.Box):
|
|||||||
game_cover = None
|
game_cover = None
|
||||||
version = 0
|
version = 0
|
||||||
|
|
||||||
def __init__(self, data, allow_side_effects=True, **kwargs):
|
def __init__(self, data, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.win = shared.win
|
self.win = shared.win
|
||||||
@@ -70,9 +70,6 @@ class Game(Gtk.Box):
|
|||||||
|
|
||||||
self.update_values(data)
|
self.update_values(data)
|
||||||
|
|
||||||
if allow_side_effects:
|
|
||||||
self.win.games[self.game_id] = self
|
|
||||||
|
|
||||||
self.set_play_icon()
|
self.set_play_icon()
|
||||||
|
|
||||||
self.event_contoller_motion = Gtk.EventControllerMotion.new()
|
self.event_contoller_motion = Gtk.EventControllerMotion.new()
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ class GameCover:
|
|||||||
stat = ImageStat.Stat(image.convert("L"))
|
stat = ImageStat.Stat(image.convert("L"))
|
||||||
|
|
||||||
# Luminance values for light and dark mode
|
# Luminance values for light and dark mode
|
||||||
self.luminance = (
|
self.luminance = [
|
||||||
(stat.mean[0] + stat.extrema[0][0]) / 510,
|
min((stat.mean[0] + stat.extrema[0][0]) / 510, 0.7),
|
||||||
(stat.mean[0] + stat.extrema[0][1]) / 510,
|
max((stat.mean[0] + stat.extrema[0][1]) / 510, 0.3),
|
||||||
)
|
]
|
||||||
else:
|
else:
|
||||||
self.blurred = self.placeholder_small
|
self.blurred = self.placeholder_small
|
||||||
self.luminance = (0.3, 0.5)
|
self.luminance = (0.3, 0.5)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from src import shared
|
|||||||
from src.errors.error_producer import ErrorProducer
|
from src.errors.error_producer import ErrorProducer
|
||||||
from src.errors.friendly_error import FriendlyError
|
from src.errors.friendly_error import FriendlyError
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import UnresolvableLocationError
|
||||||
from src.importer.sources.source import Source
|
from src.importer.sources.source import Source
|
||||||
from src.store.managers.async_manager import AsyncManager
|
from src.store.managers.async_manager import AsyncManager
|
||||||
from src.store.pipeline import Pipeline
|
from src.store.pipeline import Pipeline
|
||||||
@@ -66,7 +67,15 @@ class Importer(ErrorProducer):
|
|||||||
try:
|
try:
|
||||||
progress = progress / len(self.game_pipelines)
|
progress = progress / len(self.game_pipelines)
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
progress = 1
|
progress = 0
|
||||||
|
return progress
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sources_progress(self):
|
||||||
|
try:
|
||||||
|
progress = self.n_source_tasks_done / self.n_source_tasks_created
|
||||||
|
except ZeroDivisionError:
|
||||||
|
progress = 0
|
||||||
return progress
|
return progress
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -125,16 +134,18 @@ class Importer(ErrorProducer):
|
|||||||
source: Source
|
source: Source
|
||||||
source, *_rest = data
|
source, *_rest = data
|
||||||
|
|
||||||
# Early exit if not installed
|
# Early exit if not available or not installed
|
||||||
if not source.is_available:
|
if not source.is_available:
|
||||||
logging.info("Source %s skipped, not installed", source.id)
|
logging.info("Source %s skipped, not available", source.id)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
iterator = iter(source)
|
||||||
|
except UnresolvableLocationError:
|
||||||
|
logging.info("Source %s skipped, bad location", source.id)
|
||||||
return
|
return
|
||||||
logging.info("Scanning source %s", source.id)
|
|
||||||
|
|
||||||
# Initialize source iteration
|
|
||||||
iterator = iter(source)
|
|
||||||
|
|
||||||
# Get games from source
|
# Get games from source
|
||||||
|
logging.info("Scanning source %s", source.id)
|
||||||
while True:
|
while True:
|
||||||
# Handle exceptions raised when iterating
|
# Handle exceptions raised when iterating
|
||||||
try:
|
try:
|
||||||
@@ -172,8 +183,11 @@ class Importer(ErrorProducer):
|
|||||||
self.game_pipelines.add(pipeline)
|
self.game_pipelines.add(pipeline)
|
||||||
|
|
||||||
def update_progressbar(self):
|
def update_progressbar(self):
|
||||||
"""Update the progressbar to show the percentage of game pipelines done"""
|
"""Update the progressbar to show the overall import progress"""
|
||||||
self.progressbar.set_fraction(self.pipelines_progress)
|
# Reserve 10% for the sources discovery, the rest is the pipelines
|
||||||
|
self.progressbar.set_fraction(
|
||||||
|
(0.1 * self.sources_progress) + (0.9 * self.pipelines_progress)
|
||||||
|
)
|
||||||
|
|
||||||
def source_callback(self, _obj, _result, data):
|
def source_callback(self, _obj, _result, data):
|
||||||
"""Callback executed when a source is fully scanned"""
|
"""Callback executed when a source is fully scanned"""
|
||||||
|
|||||||
@@ -41,20 +41,20 @@ class BottlesSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
data = self.source.data_location["library.yml"].read_text("utf-8")
|
data = self.source.data_location["library.yml"].read_text("utf-8")
|
||||||
library: dict = yaml.safe_load(data)
|
library: dict = yaml.safe_load(data)
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
for entry in library.values():
|
for entry in library.values():
|
||||||
# Build game
|
# Build game
|
||||||
values = {
|
values = {
|
||||||
"version": shared.SPEC_VERSION,
|
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"added": int(time()),
|
"added": added_time,
|
||||||
"name": entry["name"],
|
"name": entry["name"],
|
||||||
"game_id": self.source.game_id_format.format(game_id=entry["id"]),
|
"game_id": self.source.game_id_format.format(game_id=entry["id"]),
|
||||||
"executable": self.source.executable_format.format(
|
"executable": self.source.executable_format.format(
|
||||||
bottle_name=entry["bottle"]["name"], game_name=entry["name"]
|
bottle_name=entry["bottle"]["name"], game_name=entry["name"]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values)
|
||||||
|
|
||||||
# Get official cover path
|
# Get official cover path
|
||||||
try:
|
try:
|
||||||
|
|||||||
134
src/importer/sources/flatpak_source.py
Normal file
134
src/importer/sources/flatpak_source.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# flatpak_source.py
|
||||||
|
#
|
||||||
|
# Copyright 2022-2023 kramo
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from time import time
|
||||||
|
import subprocess
|
||||||
|
from xdg import IconTheme
|
||||||
|
|
||||||
|
from src import shared
|
||||||
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location
|
||||||
|
from src.importer.sources.source import Source, SourceIterationResult, SourceIterator
|
||||||
|
|
||||||
|
|
||||||
|
class FlatpakSourceIterator(SourceIterator):
|
||||||
|
source: "FlatpakSource"
|
||||||
|
|
||||||
|
def generator_builder(self) -> SourceIterationResult:
|
||||||
|
"""Generator method producing games"""
|
||||||
|
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
|
IconTheme.icondirs.append(self.source.data_location["icons"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = subprocess.run(
|
||||||
|
("flatpak-spawn", "--host", "flatpak", "list", "--columns=application"),
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
flatpak_ids = process.stdout.split("\n")
|
||||||
|
|
||||||
|
to_remove = (
|
||||||
|
{"hu.kramo.Cartridges"}
|
||||||
|
if shared.schema.get_boolean("flatpak-import-launchers")
|
||||||
|
else {
|
||||||
|
"hu.kramo.Cartridges",
|
||||||
|
"com.valvesoftware.Steam",
|
||||||
|
"net.lutris.Lutris",
|
||||||
|
"com.heroicgameslauncher.hgl",
|
||||||
|
"com.usebottles.Bottles",
|
||||||
|
"io.itch.itch",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in to_remove:
|
||||||
|
if item in flatpak_ids:
|
||||||
|
flatpak_ids.remove(item)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return
|
||||||
|
|
||||||
|
for entry in (self.source.data_location["applications"]).iterdir():
|
||||||
|
flatpak_id = entry.stem
|
||||||
|
|
||||||
|
if flatpak_id not in flatpak_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
with entry.open("r", encoding="utf-8") as open_file:
|
||||||
|
string = open_file.read()
|
||||||
|
|
||||||
|
desktop_values = {"Name": None, "Icon": None, "Categories": None}
|
||||||
|
for key in desktop_values:
|
||||||
|
if regex := re.findall(f"{key}=(.*)\n", string):
|
||||||
|
desktop_values[key] = regex[0]
|
||||||
|
|
||||||
|
if not desktop_values["Name"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not desktop_values["Categories"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not "Game" in desktop_values["Categories"].split(";"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"source": self.source.id,
|
||||||
|
"added": added_time,
|
||||||
|
"name": desktop_values["Name"],
|
||||||
|
"game_id": self.source.game_id_format.format(game_id=flatpak_id),
|
||||||
|
"executable": self.source.executable_format.format(
|
||||||
|
flatpak_id=flatpak_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
game = Game(values)
|
||||||
|
|
||||||
|
additional_data = {}
|
||||||
|
if icon_name := desktop_values["Icon"]:
|
||||||
|
if icon_path := IconTheme.getIconPath(icon_name, 512):
|
||||||
|
additional_data = {"local_icon_path": Path(icon_path)}
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Produce game
|
||||||
|
yield (game, additional_data)
|
||||||
|
|
||||||
|
|
||||||
|
class FlatpakSource(Source):
|
||||||
|
"""Generic Flatpak source"""
|
||||||
|
|
||||||
|
name = "Flatpak"
|
||||||
|
iterator_class = FlatpakSourceIterator
|
||||||
|
executable_format = "flatpak run {flatpak_id}"
|
||||||
|
available_on = set(("linux",))
|
||||||
|
|
||||||
|
data_location = Location(
|
||||||
|
schema_key="flatpak-location",
|
||||||
|
candidates=(
|
||||||
|
"/var/lib/flatpak/exports/",
|
||||||
|
shared.data_dir / "flatpak" / "exports",
|
||||||
|
),
|
||||||
|
paths={
|
||||||
|
"applications": (True, "share/applications"),
|
||||||
|
"icons": (True, "share/icons"),
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -68,7 +68,7 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def game_from_library_entry(
|
def game_from_library_entry(
|
||||||
self, entry: HeroicLibraryEntry
|
self, entry: HeroicLibraryEntry, added_time: int
|
||||||
) -> SourceIterationResult:
|
) -> SourceIterationResult:
|
||||||
"""Helper method used to build a Game from a Heroic library entry"""
|
"""Helper method used to build a Game from a Heroic library entry"""
|
||||||
|
|
||||||
@@ -81,9 +81,8 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
runner = entry["runner"]
|
runner = entry["runner"]
|
||||||
service = self.sub_sources[runner]["service"]
|
service = self.sub_sources[runner]["service"]
|
||||||
values = {
|
values = {
|
||||||
"version": shared.SPEC_VERSION,
|
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"added": int(time()),
|
"added": added_time,
|
||||||
"name": entry["title"],
|
"name": entry["title"],
|
||||||
"developer": entry["developer"],
|
"developer": entry["developer"],
|
||||||
"game_id": self.source.game_id_format.format(
|
"game_id": self.source.game_id_format.format(
|
||||||
@@ -91,7 +90,7 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
),
|
),
|
||||||
"executable": self.source.executable_format.format(app_name=app_name),
|
"executable": self.source.executable_format.format(app_name=app_name),
|
||||||
}
|
}
|
||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values)
|
||||||
|
|
||||||
# Get the image path from the heroic cache
|
# Get the image path from the heroic cache
|
||||||
# Filenames are derived from the URL that heroic used to get the file
|
# Filenames are derived from the URL that heroic used to get the file
|
||||||
@@ -119,9 +118,12 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
# Invalid library.json file, skip it
|
# Invalid library.json file, skip it
|
||||||
logging.warning("Couldn't open Heroic file: %s", str(file))
|
logging.warning("Couldn't open Heroic file: %s", str(file))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
for entry in library:
|
for entry in library:
|
||||||
try:
|
try:
|
||||||
result = self.game_from_library_entry(entry)
|
result = self.game_from_library_entry(entry, added_time)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Skip invalid games
|
# Skip invalid games
|
||||||
logging.warning("Invalid Heroic game skipped in %s", str(file))
|
logging.warning("Invalid Heroic game skipped in %s", str(file))
|
||||||
@@ -130,7 +132,7 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
|
|
||||||
class HeroicSource(URLExecutableSource):
|
class HeroicSource(URLExecutableSource):
|
||||||
"""Generic heroic games launcher source"""
|
"""Generic Heroic Games Launcher source"""
|
||||||
|
|
||||||
name = "Heroic"
|
name = "Heroic"
|
||||||
iterator_class = HeroicSourceIterator
|
iterator_class = HeroicSourceIterator
|
||||||
@@ -141,9 +143,8 @@ class HeroicSource(URLExecutableSource):
|
|||||||
schema_key="heroic-location",
|
schema_key="heroic-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
"~/.var/app/com.heroicgameslauncher.hgl/config/heroic/",
|
"~/.var/app/com.heroicgameslauncher.hgl/config/heroic/",
|
||||||
shared.config_dir / "heroic/",
|
shared.config_dir / "heroic",
|
||||||
"~/.config/heroic/",
|
shared.appdata_dir / "heroic",
|
||||||
shared.appdata_dir / "heroic/",
|
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"config.json": (False, "config.json"),
|
"config.json": (False, "config.json"),
|
||||||
|
|||||||
@@ -59,18 +59,19 @@ class ItchSourceIterator(SourceIterator):
|
|||||||
connection = connect(db_path)
|
connection = connect(db_path)
|
||||||
cursor = connection.execute(db_request)
|
cursor = connection.execute(db_request)
|
||||||
|
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
# Create games from the db results
|
# Create games from the db results
|
||||||
for row in cursor:
|
for row in cursor:
|
||||||
values = {
|
values = {
|
||||||
"version": shared.SPEC_VERSION,
|
"added": added_time,
|
||||||
"added": int(time()),
|
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"name": row[1],
|
"name": row[1],
|
||||||
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
"game_id": self.source.game_id_format.format(game_id=row[0]),
|
||||||
"executable": self.source.executable_format.format(cave_id=row[4]),
|
"executable": self.source.executable_format.format(cave_id=row[4]),
|
||||||
}
|
}
|
||||||
additional_data = {"online_cover_url": row[3] or row[2]}
|
additional_data = {"online_cover_url": row[3] or row[2]}
|
||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values)
|
||||||
yield (game, additional_data)
|
yield (game, additional_data)
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
@@ -87,9 +88,8 @@ class ItchSource(URLExecutableSource):
|
|||||||
schema_key="itch-location",
|
schema_key="itch-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
"~/.var/app/io.itch.itch/config/itch/",
|
"~/.var/app/io.itch.itch/config/itch/",
|
||||||
shared.config_dir / "itch/",
|
shared.config_dir / "itch",
|
||||||
"~/.config/itch/",
|
shared.appdata_dir / "itch",
|
||||||
shared.appdata_dir / "itch/",
|
|
||||||
),
|
),
|
||||||
paths={"butler.db": (False, "db/butler.db")},
|
paths={"butler.db": (False, "db/butler.db")},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ from src.importer.sources.source import Source, SourceIterationResult, SourceIte
|
|||||||
class LegendarySourceIterator(SourceIterator):
|
class LegendarySourceIterator(SourceIterator):
|
||||||
source: "LegendarySource"
|
source: "LegendarySource"
|
||||||
|
|
||||||
def game_from_library_entry(self, entry: dict) -> SourceIterationResult:
|
def game_from_library_entry(
|
||||||
|
self, entry: dict, added_time: int
|
||||||
|
) -> SourceIterationResult:
|
||||||
# Skip non-games
|
# Skip non-games
|
||||||
if entry["is_dlc"]:
|
if entry["is_dlc"]:
|
||||||
return None
|
return None
|
||||||
@@ -40,8 +42,7 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
# Build game
|
# Build game
|
||||||
app_name = entry["app_name"]
|
app_name = entry["app_name"]
|
||||||
values = {
|
values = {
|
||||||
"version": shared.SPEC_VERSION,
|
"added": added_time,
|
||||||
"added": int(time()),
|
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"name": entry["title"],
|
"name": entry["title"],
|
||||||
"game_id": self.source.game_id_format.format(game_id=app_name),
|
"game_id": self.source.game_id_format.format(game_id=app_name),
|
||||||
@@ -61,7 +62,7 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
except (JSONDecodeError, OSError, KeyError):
|
except (JSONDecodeError, OSError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values)
|
||||||
return (game, data)
|
return (game, data)
|
||||||
|
|
||||||
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
||||||
@@ -72,10 +73,13 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
except (JSONDecodeError, OSError):
|
except (JSONDecodeError, OSError):
|
||||||
logging.warning("Couldn't open Legendary file: %s", str(file))
|
logging.warning("Couldn't open Legendary file: %s", str(file))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
# Generate games from library
|
# Generate games from library
|
||||||
for entry in library.values():
|
for entry in library.values():
|
||||||
try:
|
try:
|
||||||
result = self.game_from_library_entry(entry)
|
result = self.game_from_library_entry(entry, added_time)
|
||||||
except KeyError as error:
|
except KeyError as error:
|
||||||
# Skip invalid games
|
# Skip invalid games
|
||||||
logging.warning(
|
logging.warning(
|
||||||
@@ -93,10 +97,7 @@ class LegendarySource(Source):
|
|||||||
iterator_class = LegendarySourceIterator
|
iterator_class = LegendarySourceIterator
|
||||||
data_location: Location = Location(
|
data_location: Location = Location(
|
||||||
schema_key="legendary-location",
|
schema_key="legendary-location",
|
||||||
candidates=(
|
candidates=(shared.config_dir / "legendary",),
|
||||||
shared.config_dir / "legendary/",
|
|
||||||
"~/.config/legendary",
|
|
||||||
),
|
|
||||||
paths={
|
paths={
|
||||||
"installed.json": (False, "installed.json"),
|
"installed.json": (False, "installed.json"),
|
||||||
"metadata": (True, "metadata"),
|
"metadata": (True, "metadata"),
|
||||||
|
|||||||
@@ -48,19 +48,24 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
AND configPath IS NOT NULL
|
AND configPath IS NOT NULL
|
||||||
AND installed
|
AND installed
|
||||||
AND (runner IS NOT "steam" OR :import_steam)
|
AND (runner IS NOT "steam" OR :import_steam)
|
||||||
|
AND (runner IS NOT "flatpak" OR :import_flatpak)
|
||||||
;
|
;
|
||||||
"""
|
"""
|
||||||
params = {"import_steam": shared.schema.get_boolean("lutris-import-steam")}
|
params = {
|
||||||
|
"import_steam": shared.schema.get_boolean("lutris-import-steam"),
|
||||||
|
"import_flatpak": shared.schema.get_boolean("lutris-import-flatpak"),
|
||||||
|
}
|
||||||
db_path = copy_db(self.source.data_location["pga.db"])
|
db_path = copy_db(self.source.data_location["pga.db"])
|
||||||
connection = connect(db_path)
|
connection = connect(db_path)
|
||||||
cursor = connection.execute(request, params)
|
cursor = connection.execute(request, params)
|
||||||
|
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
# Create games from the DB results
|
# Create games from the DB results
|
||||||
for row in cursor:
|
for row in cursor:
|
||||||
# Create game
|
# Create game
|
||||||
values = {
|
values = {
|
||||||
"version": shared.SPEC_VERSION,
|
"added": added_time,
|
||||||
"added": int(time()),
|
|
||||||
"hidden": row[4],
|
"hidden": row[4],
|
||||||
"name": row[1],
|
"name": row[1],
|
||||||
"source": f"{self.source.id}_{row[3]}",
|
"source": f"{self.source.id}_{row[3]}",
|
||||||
@@ -69,7 +74,7 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
),
|
),
|
||||||
"executable": self.source.executable_format.format(game_id=row[2]),
|
"executable": self.source.executable_format.format(game_id=row[2]),
|
||||||
}
|
}
|
||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values)
|
||||||
|
|
||||||
# Get official image path
|
# Get official image path
|
||||||
image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg"
|
image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg"
|
||||||
@@ -83,7 +88,7 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
|
|
||||||
class LutrisSource(URLExecutableSource):
|
class LutrisSource(URLExecutableSource):
|
||||||
"""Generic lutris source"""
|
"""Generic Lutris source"""
|
||||||
|
|
||||||
name = "Lutris"
|
name = "Lutris"
|
||||||
iterator_class = LutrisSourceIterator
|
iterator_class = LutrisSourceIterator
|
||||||
@@ -96,8 +101,7 @@ class LutrisSource(URLExecutableSource):
|
|||||||
schema_key="lutris-location",
|
schema_key="lutris-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
"~/.var/app/net.lutris.Lutris/data/lutris/",
|
"~/.var/app/net.lutris.Lutris/data/lutris/",
|
||||||
shared.data_dir / "lutris/",
|
shared.data_dir / "lutris",
|
||||||
"~/.local/share/lutris/",
|
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"pga.db": (False, "pga.db"),
|
"pga.db": (False, "pga.db"),
|
||||||
@@ -108,8 +112,7 @@ class LutrisSource(URLExecutableSource):
|
|||||||
schema_key="lutris-cache-location",
|
schema_key="lutris-cache-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
"~/.var/app/net.lutris.Lutris/cache/lutris/",
|
"~/.var/app/net.lutris.Lutris/cache/lutris/",
|
||||||
shared.cache_dir / "lutris/",
|
shared.cache_dir / "lutris",
|
||||||
"~/.cache/lutris",
|
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
"coverart": (True, "coverart"),
|
"coverart": (True, "coverart"),
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ from abc import abstractmethod
|
|||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from typing import Any, Generator, Optional
|
from typing import Any, Generator, Optional
|
||||||
|
|
||||||
from src.errors.friendly_error import FriendlyError
|
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.importer.sources.location import Location, UnresolvableLocationError
|
from src.importer.sources.location import Location
|
||||||
|
|
||||||
# Type of the data returned by iterating on a Source
|
# Type of the data returned by iterating on a Source
|
||||||
SourceIterationResult = None | Game | tuple[Game, tuple[Any]]
|
SourceIterationResult = None | Game | tuple[Game, tuple[Any]]
|
||||||
@@ -88,7 +87,7 @@ class Source(Iterable):
|
|||||||
@property
|
@property
|
||||||
def game_id_format(self) -> str:
|
def game_id_format(self) -> str:
|
||||||
"""The string format used to construct game IDs"""
|
"""The string format used to construct game IDs"""
|
||||||
return self.name.lower() + "_{game_id}"
|
return self.id + "_{game_id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
@@ -100,27 +99,15 @@ class Source(Iterable):
|
|||||||
"""The executable format used to construct game executables"""
|
"""The executable format used to construct game executables"""
|
||||||
|
|
||||||
def __iter__(self) -> SourceIterator:
|
def __iter__(self) -> SourceIterator:
|
||||||
"""Get an iterator for the source"""
|
"""
|
||||||
for location_name in (
|
Get an iterator for the source
|
||||||
locations := {
|
:raises UnresolvableLocationError: Not iterable if any of the locations are unresolvable
|
||||||
"data": "Data",
|
"""
|
||||||
"cache": "Cache",
|
for location_name in ("data", "cache", "config"):
|
||||||
"config": "Configuration",
|
|
||||||
}.keys()
|
|
||||||
):
|
|
||||||
location = getattr(self, f"{location_name}_location", None)
|
location = getattr(self, f"{location_name}_location", None)
|
||||||
if location is None:
|
if location is None:
|
||||||
continue
|
continue
|
||||||
try:
|
location.resolve()
|
||||||
location.resolve()
|
|
||||||
except UnresolvableLocationError as error:
|
|
||||||
raise FriendlyError(
|
|
||||||
# The variables are the type of location (eg. cache) and the source's name (eg. Steam)
|
|
||||||
"Invalid {} Location for {{}}".format(locations[location_name]),
|
|
||||||
"Pick a new one or disable the source in preferences",
|
|
||||||
(self.name,),
|
|
||||||
(self.name,),
|
|
||||||
) from error
|
|
||||||
return self.iterator_class(self)
|
return self.iterator_class(self)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
"""Generator method producing games"""
|
"""Generator method producing games"""
|
||||||
appid_cache = set()
|
appid_cache = set()
|
||||||
manifests = self.get_manifests()
|
manifests = self.get_manifests()
|
||||||
|
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
for manifest in manifests:
|
for manifest in manifests:
|
||||||
# Get metadata from manifest
|
# Get metadata from manifest
|
||||||
steam = SteamFileHelper()
|
steam = SteamFileHelper()
|
||||||
@@ -87,14 +90,13 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
# Build game from local data
|
# Build game from local data
|
||||||
values = {
|
values = {
|
||||||
"version": shared.SPEC_VERSION,
|
"added": added_time,
|
||||||
"added": int(time()),
|
|
||||||
"name": local_data["name"],
|
"name": local_data["name"],
|
||||||
"source": self.source.id,
|
"source": self.source.id,
|
||||||
"game_id": self.source.game_id_format.format(game_id=appid),
|
"game_id": self.source.game_id_format.format(game_id=appid),
|
||||||
"executable": self.source.executable_format.format(game_id=appid),
|
"executable": self.source.executable_format.format(game_id=appid),
|
||||||
}
|
}
|
||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values)
|
||||||
|
|
||||||
# Add official cover image
|
# Add official cover image
|
||||||
image_path = (
|
image_path = (
|
||||||
@@ -117,8 +119,8 @@ class SteamSource(URLExecutableSource):
|
|||||||
schema_key="steam-location",
|
schema_key="steam-location",
|
||||||
candidates=(
|
candidates=(
|
||||||
"~/.var/app/com.valvesoftware.Steam/data/Steam/",
|
"~/.var/app/com.valvesoftware.Steam/data/Steam/",
|
||||||
shared.data_dir / "Steam/",
|
shared.data_dir / "Steam",
|
||||||
"~/.steam/",
|
"~/.steam",
|
||||||
shared.programfiles32_dir / "Steam",
|
shared.programfiles32_dir / "Steam",
|
||||||
),
|
),
|
||||||
paths={
|
paths={
|
||||||
|
|||||||
115
src/importer/sources/yuzu_source.py
Normal file
115
src/importer/sources/yuzu_source.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# lutris_source.py
|
||||||
|
#
|
||||||
|
# Copyright 2022-2023 kramo
|
||||||
|
# Copyright 2023 Geoffrey Coulaud
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
from time import time
|
||||||
|
from typing import Generator, Iterable
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
from src import shared
|
||||||
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location
|
||||||
|
from src.importer.sources.source import (
|
||||||
|
SourceIterationResult,
|
||||||
|
SourceIterator,
|
||||||
|
Source,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class YuzuSourceIterator(SourceIterator):
|
||||||
|
source: "YuzuSource"
|
||||||
|
|
||||||
|
extensions = (".xci", ".nsp", ".nso", ".nro")
|
||||||
|
|
||||||
|
def iter_game_dirs(self) -> Iterable[tuple[bool, Path]]:
|
||||||
|
"""
|
||||||
|
Get the rom directories from the parsed config
|
||||||
|
|
||||||
|
The returned tuple indicates if the dir should be scanned recursively,
|
||||||
|
then its path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the config data
|
||||||
|
config = ConfigParser()
|
||||||
|
if not config.read(
|
||||||
|
self.source.data_location["qt-config.ini"], encoding="utf-8"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Iterate through the dirs
|
||||||
|
n_dirs = config.getint("UI", r"Paths\gamedirs\size", fallback=0)
|
||||||
|
for i in range(1, n_dirs + 1):
|
||||||
|
deep = config.getboolean(
|
||||||
|
"UI", f"Paths\\gamedirs\\{i}\\deep_scan", fallback=False
|
||||||
|
)
|
||||||
|
path = Path(config.get("UI", f"Paths\\gamedirs\\{i}\\path", fallback=None))
|
||||||
|
if path is None:
|
||||||
|
continue
|
||||||
|
yield deep, path
|
||||||
|
|
||||||
|
def iter_rom_files(
|
||||||
|
self, root: Path, recursive: bool = False
|
||||||
|
) -> Generator[Path, None, None]:
|
||||||
|
"""Generator method to iterate through rom files"""
|
||||||
|
if not recursive:
|
||||||
|
for path in root.iterdir():
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if not path.suffix in self.extensions:
|
||||||
|
continue
|
||||||
|
yield path
|
||||||
|
else:
|
||||||
|
for dir_path, _dirs, file_names in os.walk(root):
|
||||||
|
for filename in file_names:
|
||||||
|
path = Path(dir_path) / filename
|
||||||
|
if path.suffix in self.extensions:
|
||||||
|
continue
|
||||||
|
yield path
|
||||||
|
|
||||||
|
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
||||||
|
"""Generator method producing games"""
|
||||||
|
|
||||||
|
added_time = int(time())
|
||||||
|
|
||||||
|
# Get the games
|
||||||
|
for recursive_search, game_dir in self.iter_game_dirs():
|
||||||
|
for path in self.iter_rom_files(game_dir, recursive_search):
|
||||||
|
values = {
|
||||||
|
# TODO add game_id
|
||||||
|
"added": added_time,
|
||||||
|
"source": self.source.id,
|
||||||
|
"executable": f"yuzu {str(path)}", # HACK change depending on the variant
|
||||||
|
}
|
||||||
|
game = Game(values)
|
||||||
|
additional_data = {}
|
||||||
|
yield game, additional_data
|
||||||
|
|
||||||
|
|
||||||
|
class YuzuSource(Source):
|
||||||
|
config_location = Location(
|
||||||
|
"yuzu-location",
|
||||||
|
(
|
||||||
|
"~/.var/app/org.yuzu_emu.yuzu/config/yuzu",
|
||||||
|
shared.config_dir / "yuzu",
|
||||||
|
"~/.config/yuzu",
|
||||||
|
# TODO windows path
|
||||||
|
),
|
||||||
|
{"qt-config.ini": (False, "qt-config.ini")},
|
||||||
|
)
|
||||||
@@ -33,6 +33,8 @@ class SessionFileHandler(StreamHandler):
|
|||||||
The files are compressed and older sessions logs are kept up to a small limit.
|
The files are compressed and older sessions logs are kept up to a small limit.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
NUMBER_SUFFIX_POSITION = 1
|
||||||
|
|
||||||
backup_count: int
|
backup_count: int
|
||||||
filename: Path
|
filename: Path
|
||||||
log_file: StringIO = None
|
log_file: StringIO = None
|
||||||
@@ -41,50 +43,75 @@ class SessionFileHandler(StreamHandler):
|
|||||||
"""Create the log dir if needed"""
|
"""Create the log dir if needed"""
|
||||||
self.filename.parent.mkdir(exist_ok=True, parents=True)
|
self.filename.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
def path_is_logfile(self, path: Path) -> bool:
|
||||||
|
return path.is_file() and path.name.startswith(self.filename.stem)
|
||||||
|
|
||||||
|
def path_has_number(self, path: Path) -> bool:
|
||||||
|
try:
|
||||||
|
int(path.suffixes[self.NUMBER_SUFFIX_POSITION][1:])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_path_number(self, path: Path) -> int:
|
||||||
|
"""Get the number extension in the filename as an int"""
|
||||||
|
suffixes = path.suffixes
|
||||||
|
number = (
|
||||||
|
0
|
||||||
|
if not self.path_has_number(path)
|
||||||
|
else int(suffixes[self.NUMBER_SUFFIX_POSITION][1:])
|
||||||
|
)
|
||||||
|
return number
|
||||||
|
|
||||||
|
def set_path_number(self, path: Path, number: int) -> str:
|
||||||
|
"""Set or add the number extension in the filename"""
|
||||||
|
suffixes = path.suffixes
|
||||||
|
if self.path_has_number(path):
|
||||||
|
suffixes.pop(self.NUMBER_SUFFIX_POSITION)
|
||||||
|
suffixes.insert(self.NUMBER_SUFFIX_POSITION, f".{number}")
|
||||||
|
stem = path.name.split(".", maxsplit=1)[0]
|
||||||
|
new_name = stem + "".join(suffixes)
|
||||||
|
return new_name
|
||||||
|
|
||||||
|
def file_sort_key(self, path: Path) -> int:
|
||||||
|
"""Key function used to sort files"""
|
||||||
|
return self.get_path_number(path) if self.path_has_number(path) else 0
|
||||||
|
|
||||||
|
def get_logfiles(self) -> list[Path]:
|
||||||
|
"""Get the log files"""
|
||||||
|
logfiles = list(filter(self.path_is_logfile, self.filename.parent.iterdir()))
|
||||||
|
logfiles.sort(key=self.file_sort_key, reverse=True)
|
||||||
|
return logfiles
|
||||||
|
|
||||||
def rotate_file(self, path: Path):
|
def rotate_file(self, path: Path):
|
||||||
"""Rotate a file's number suffix and remove it if it's too old"""
|
"""Rotate a file's number suffix and remove it if it's too old"""
|
||||||
|
|
||||||
# Skip non interesting dir entries
|
# If uncompressed, compress
|
||||||
if not (path.is_file() and path.name.startswith(self.filename.name)):
|
if not path.name.endswith(".xz"):
|
||||||
return
|
new_path = path.with_suffix(path.suffix + ".xz")
|
||||||
|
with (
|
||||||
# Compute the new number suffix
|
lzma.open(
|
||||||
suffixes = path.suffixes
|
new_path, "wt", format=FORMAT_XZ, preset=PRESET_DEFAULT
|
||||||
has_number = len(suffixes) != len(self.filename.suffixes)
|
) as lzma_file,
|
||||||
current_number = 0 if not has_number else int(suffixes[-1][1:])
|
open(path, "r", encoding="utf-8") as original_file,
|
||||||
new_number = current_number + 1
|
):
|
||||||
|
lzma_file.write(original_file.read())
|
||||||
|
path.unlink()
|
||||||
|
path = new_path
|
||||||
|
|
||||||
# Rename with new number suffix
|
# Rename with new number suffix
|
||||||
if has_number:
|
new_number = self.get_path_number(path) + 1
|
||||||
suffixes.pop()
|
new_path_name = self.set_path_number(path, new_number)
|
||||||
suffixes.append(f".{new_number}")
|
path = path.rename(path.with_name(new_path_name))
|
||||||
stem = path.name.split(".", maxsplit=1)[0]
|
|
||||||
new_name = stem + "".join(suffixes)
|
|
||||||
path = path.rename(path.with_name(new_name))
|
|
||||||
|
|
||||||
# Remove older files
|
# Remove older files
|
||||||
if new_number > self.backup_count:
|
if new_number > self.backup_count:
|
||||||
path.unlink()
|
path.unlink()
|
||||||
return
|
return
|
||||||
|
|
||||||
def file_sort_key(self, path: Path) -> int:
|
|
||||||
"""Key function used to sort files"""
|
|
||||||
if not path.name.startswith(self.filename.name):
|
|
||||||
# First all files that aren't logs
|
|
||||||
return -1
|
|
||||||
if path.name == self.filename.name:
|
|
||||||
# Then the latest log file
|
|
||||||
return 0
|
|
||||||
# Then in order the other log files
|
|
||||||
return int(path.suffixes[-1][1:])
|
|
||||||
|
|
||||||
def get_files(self) -> list[Path]:
|
|
||||||
return list(self.filename.parent.iterdir())
|
|
||||||
|
|
||||||
def rotate(self) -> None:
|
def rotate(self) -> None:
|
||||||
"""Rotate the numbered suffix on the log files and remove old ones"""
|
"""Rotate the numbered suffix on the log files and remove old ones"""
|
||||||
(files := self.get_files()).sort(key=self.file_sort_key, reverse=True)
|
for path in self.get_logfiles():
|
||||||
for path in files:
|
|
||||||
self.rotate_file(path)
|
self.rotate_file(path)
|
||||||
|
|
||||||
def __init__(self, filename: PathLike, backup_count: int = 2) -> None:
|
def __init__(self, filename: PathLike, backup_count: int = 2) -> None:
|
||||||
@@ -92,10 +119,8 @@ class SessionFileHandler(StreamHandler):
|
|||||||
self.backup_count = backup_count
|
self.backup_count = backup_count
|
||||||
self.create_dir()
|
self.create_dir()
|
||||||
self.rotate()
|
self.rotate()
|
||||||
shared.log_files = self.get_files()
|
self.log_file = open(self.filename, "w", encoding="utf-8")
|
||||||
self.log_file = lzma.open(
|
shared.log_files = self.get_logfiles()
|
||||||
self.filename, "at", format=FORMAT_XZ, preset=PRESET_DEFAULT
|
|
||||||
)
|
|
||||||
super().__init__(self.log_file)
|
super().__init__(self.log_file)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import logging.config as logging_dot_config
|
import logging.config as logging_dot_config
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -35,13 +36,14 @@ def setup_logging():
|
|||||||
app_log_level = os.environ.get("LOGLEVEL", profile_app_log_level).upper()
|
app_log_level = os.environ.get("LOGLEVEL", profile_app_log_level).upper()
|
||||||
lib_log_level = os.environ.get("LIBLOGLEVEL", profile_lib_log_level).upper()
|
lib_log_level = os.environ.get("LIBLOGLEVEL", profile_lib_log_level).upper()
|
||||||
|
|
||||||
log_filename = shared.cache_dir / "cartridges" / "logs" / "cartridges.log.xz"
|
log_filename = shared.cache_dir / "cartridges" / "logs" / "cartridges.log"
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"formatters": {
|
"formatters": {
|
||||||
"file_formatter": {
|
"file_formatter": {
|
||||||
"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
|
"format": "%(asctime)s - %(levelname)s: %(message)s",
|
||||||
|
"datefmt": "%M:%S",
|
||||||
},
|
},
|
||||||
"console_formatter": {
|
"console_formatter": {
|
||||||
"format": "%(name)s %(levelname)s - %(message)s",
|
"format": "%(name)s %(levelname)s - %(message)s",
|
||||||
@@ -91,9 +93,8 @@ def log_system_info():
|
|||||||
"""Log system debug information"""
|
"""Log system debug information"""
|
||||||
|
|
||||||
logging.debug("Starting %s v%s (%s)", shared.APP_ID, shared.VERSION, shared.PROFILE)
|
logging.debug("Starting %s v%s (%s)", shared.APP_ID, shared.VERSION, shared.PROFILE)
|
||||||
logging.debug("System: %s", sys.platform)
|
|
||||||
logging.debug("Python version: %s", sys.version)
|
logging.debug("Python version: %s", sys.version)
|
||||||
if os.getenv("FLATPAK_ID"):
|
if os.getenv("FLATPAK_ID") == shared.APP_ID:
|
||||||
process = subprocess.run(
|
process = subprocess.run(
|
||||||
("flatpak-spawn", "--host", "flatpak", "--version"),
|
("flatpak-spawn", "--host", "flatpak", "--version"),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -101,11 +102,8 @@ def log_system_info():
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
logging.debug("Flatpak version: %s", process.stdout.rstrip())
|
logging.debug("Flatpak version: %s", process.stdout.rstrip())
|
||||||
|
logging.debug("Platform: %s", platform.platform())
|
||||||
if os.name == "posix":
|
if os.name == "posix":
|
||||||
uname = os.uname()
|
for key, value in platform.uname()._asdict().items():
|
||||||
logging.debug("Uname info:")
|
logging.debug("\t%s: %s", key.title(), value)
|
||||||
logging.debug("\tsysname: %s", uname.sysname)
|
logging.debug("─" * 37)
|
||||||
logging.debug("\trelease: %s", uname.release)
|
|
||||||
logging.debug("\tversion: %s", uname.version)
|
|
||||||
logging.debug("\tmachine: %s", uname.machine)
|
|
||||||
logging.debug("-" * 80)
|
|
||||||
|
|||||||
30
src/main.py
30
src/main.py
@@ -34,6 +34,7 @@ from src.details_window import DetailsWindow
|
|||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.importer.importer import Importer
|
from src.importer.importer import Importer
|
||||||
from src.importer.sources.bottles_source import BottlesSource
|
from src.importer.sources.bottles_source import BottlesSource
|
||||||
|
from src.importer.sources.flatpak_source import FlatpakSource
|
||||||
from src.importer.sources.heroic_source import HeroicSource
|
from src.importer.sources.heroic_source import HeroicSource
|
||||||
from src.importer.sources.itch_source import ItchSource
|
from src.importer.sources.itch_source import ItchSource
|
||||||
from src.importer.sources.legendary_source import LegendarySource
|
from src.importer.sources.legendary_source import LegendarySource
|
||||||
@@ -55,6 +56,7 @@ class CartridgesApplication(Adw.Application):
|
|||||||
win = None
|
win = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
shared.store = Store()
|
||||||
super().__init__(
|
super().__init__(
|
||||||
application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE
|
application_id=shared.APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE
|
||||||
)
|
)
|
||||||
@@ -81,12 +83,9 @@ class CartridgesApplication(Adw.Application):
|
|||||||
"is-maximized", self.win, "maximized", Gio.SettingsBindFlags.DEFAULT
|
"is-maximized", self.win, "maximized", Gio.SettingsBindFlags.DEFAULT
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the games store ready to load games from disk
|
# Load games from disk
|
||||||
if not shared.store:
|
shared.store.add_manager(FileManager(), False)
|
||||||
shared.store = Store()
|
shared.store.add_manager(DisplayManager())
|
||||||
shared.store.add_manager(FileManager(), False)
|
|
||||||
shared.store.add_manager(DisplayManager())
|
|
||||||
|
|
||||||
self.load_games_from_disk()
|
self.load_games_from_disk()
|
||||||
|
|
||||||
# Add rest of the managers for game imports
|
# Add rest of the managers for game imports
|
||||||
@@ -139,14 +138,24 @@ class CartridgesApplication(Adw.Application):
|
|||||||
if shared.games_dir.is_dir():
|
if shared.games_dir.is_dir():
|
||||||
for game_file in shared.games_dir.iterdir():
|
for game_file in shared.games_dir.iterdir():
|
||||||
data = json.load(game_file.open())
|
data = json.load(game_file.open())
|
||||||
game = Game(data, allow_side_effects=False)
|
game = Game(data)
|
||||||
shared.store.add_game(game, {"skip_save": True})
|
shared.store.add_game(game, {"skip_save": True})
|
||||||
|
|
||||||
def on_about_action(self, *_args):
|
def on_about_action(self, *_args):
|
||||||
|
# Get the debug info from the log files
|
||||||
debug_str = ""
|
debug_str = ""
|
||||||
for path in shared.log_files:
|
for i, path in enumerate(shared.log_files):
|
||||||
log_file = lzma.open(path, "rt", encoding="utf-8")
|
# Add a horizontal line between runs
|
||||||
|
if i > 0:
|
||||||
|
debug_str += "─" * 37 + "\n"
|
||||||
|
# Add the run's logs
|
||||||
|
log_file = (
|
||||||
|
lzma.open(path, "rt", encoding="utf-8")
|
||||||
|
if path.name.endswith(".xz")
|
||||||
|
else open(path, "r", encoding="utf-8")
|
||||||
|
)
|
||||||
debug_str += log_file.read()
|
debug_str += log_file.read()
|
||||||
|
log_file.close()
|
||||||
|
|
||||||
about = Adw.AboutWindow(
|
about = Adw.AboutWindow(
|
||||||
transient_for=self.win,
|
transient_for=self.win,
|
||||||
@@ -212,6 +221,9 @@ class CartridgesApplication(Adw.Application):
|
|||||||
if shared.schema.get_boolean("bottles"):
|
if shared.schema.get_boolean("bottles"):
|
||||||
importer.add_source(BottlesSource())
|
importer.add_source(BottlesSource())
|
||||||
|
|
||||||
|
if shared.schema.get_boolean("flatpak"):
|
||||||
|
importer.add_source(FlatpakSource())
|
||||||
|
|
||||||
if shared.schema.get_boolean("itch"):
|
if shared.schema.get_boolean("itch"):
|
||||||
importer.add_source(ItchSource())
|
importer.add_source(ItchSource())
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from gi.repository import Adw, Gio, GLib, Gtk
|
|||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.importer.sources.bottles_source import BottlesSource
|
from src.importer.sources.bottles_source import BottlesSource
|
||||||
|
from src.importer.sources.flatpak_source import FlatpakSource
|
||||||
from src.importer.sources.heroic_source import HeroicSource
|
from src.importer.sources.heroic_source import HeroicSource
|
||||||
from src.importer.sources.itch_source import ItchSource
|
from src.importer.sources.itch_source import ItchSource
|
||||||
from src.importer.sources.legendary_source import LegendarySource
|
from src.importer.sources.legendary_source import LegendarySource
|
||||||
@@ -59,6 +60,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
lutris_cache_action_row = Gtk.Template.Child()
|
lutris_cache_action_row = Gtk.Template.Child()
|
||||||
lutris_cache_file_chooser_button = Gtk.Template.Child()
|
lutris_cache_file_chooser_button = Gtk.Template.Child()
|
||||||
lutris_import_steam_switch = Gtk.Template.Child()
|
lutris_import_steam_switch = Gtk.Template.Child()
|
||||||
|
lutris_import_flatpak_switch = Gtk.Template.Child()
|
||||||
|
|
||||||
heroic_expander_row = Gtk.Template.Child()
|
heroic_expander_row = Gtk.Template.Child()
|
||||||
heroic_config_action_row = Gtk.Template.Child()
|
heroic_config_action_row = Gtk.Template.Child()
|
||||||
@@ -79,6 +81,11 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
legendary_config_action_row = Gtk.Template.Child()
|
legendary_config_action_row = Gtk.Template.Child()
|
||||||
legendary_config_file_chooser_button = Gtk.Template.Child()
|
legendary_config_file_chooser_button = Gtk.Template.Child()
|
||||||
|
|
||||||
|
flatpak_expander_row = Gtk.Template.Child()
|
||||||
|
flatpak_data_action_row = Gtk.Template.Child()
|
||||||
|
flatpak_data_file_chooser_button = Gtk.Template.Child()
|
||||||
|
flatpak_import_launchers_switch = Gtk.Template.Child()
|
||||||
|
|
||||||
sgdb_key_group = Gtk.Template.Child()
|
sgdb_key_group = Gtk.Template.Child()
|
||||||
sgdb_key_entry_row = Gtk.Template.Child()
|
sgdb_key_entry_row = Gtk.Template.Child()
|
||||||
sgdb_switch = Gtk.Template.Child()
|
sgdb_switch = Gtk.Template.Child()
|
||||||
@@ -124,6 +131,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
# Sources settings
|
# Sources settings
|
||||||
for source_class in (
|
for source_class in (
|
||||||
BottlesSource,
|
BottlesSource,
|
||||||
|
FlatpakSource,
|
||||||
HeroicSource,
|
HeroicSource,
|
||||||
ItchSource,
|
ItchSource,
|
||||||
LegendarySource,
|
LegendarySource,
|
||||||
@@ -168,9 +176,11 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
"cover-launches-game",
|
"cover-launches-game",
|
||||||
"high-quality-images",
|
"high-quality-images",
|
||||||
"lutris-import-steam",
|
"lutris-import-steam",
|
||||||
|
"lutris-import-flatpak",
|
||||||
"heroic-import-epic",
|
"heroic-import-epic",
|
||||||
"heroic-import-gog",
|
"heroic-import-gog",
|
||||||
"heroic-import-sideload",
|
"heroic-import-sideload",
|
||||||
|
"flatpak-import-launchers",
|
||||||
"sgdb",
|
"sgdb",
|
||||||
"sgdb-prefer",
|
"sgdb-prefer",
|
||||||
"sgdb-animated",
|
"sgdb-animated",
|
||||||
@@ -202,7 +212,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
self.toast.dismiss()
|
self.toast.dismiss()
|
||||||
|
|
||||||
def remove_all_games(self, *_args):
|
def remove_all_games(self, *_args):
|
||||||
for game in self.win.games.values():
|
for game in shared.store.games.values():
|
||||||
if not game.removed:
|
if not game.removed:
|
||||||
self.removed_games.add(game)
|
self.removed_games.add(game)
|
||||||
|
|
||||||
@@ -215,7 +225,7 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
|
|
||||||
self.add_toast(self.toast)
|
self.add_toast(self.toast)
|
||||||
|
|
||||||
def reset_app(*_args):
|
def reset_app(self, *_args):
|
||||||
rmtree(shared.data_dir / "cartridges", True)
|
rmtree(shared.data_dir / "cartridges", True)
|
||||||
rmtree(shared.config_dir / "cartridges", True)
|
rmtree(shared.config_dir / "cartridges", True)
|
||||||
rmtree(shared.cache_dir / "cartridges", True)
|
rmtree(shared.cache_dir / "cartridges", True)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from src import shared
|
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.game_cover import GameCover
|
from src.game_cover import GameCover
|
||||||
from src.store.managers.manager import Manager
|
from src.store.managers.manager import Manager
|
||||||
@@ -32,7 +31,6 @@ class DisplayManager(Manager):
|
|||||||
signals = {"update-ready"}
|
signals = {"update-ready"}
|
||||||
|
|
||||||
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
def manager_logic(self, game: Game, _additional_data: dict) -> None:
|
||||||
shared.win.games[game.game_id] = game
|
|
||||||
if game.get_parent():
|
if game.get_parent():
|
||||||
game.get_parent().get_parent().remove(game)
|
game.get_parent().get_parent().remove(game)
|
||||||
if game.get_parent():
|
if game.get_parent():
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# local_cover_manager.py
|
# local_cover_manager.py
|
||||||
#
|
#
|
||||||
# Copyright 2023 Geoffrey Coulaud
|
# Copyright 2023 Geoffrey Coulaud
|
||||||
|
# Copyright 2023 kramo
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -17,12 +18,13 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from pathlib import Path
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
from src.store.managers.manager import Manager
|
from src.store.managers.manager import Manager
|
||||||
from src.store.managers.steam_api_manager import SteamAPIManager
|
from src.store.managers.steam_api_manager import SteamAPIManager
|
||||||
from src.utils.save_cover import save_cover, resize_cover
|
from src.utils.save_cover import resize_cover, save_cover
|
||||||
|
|
||||||
|
|
||||||
class LocalCoverManager(Manager):
|
class LocalCoverManager(Manager):
|
||||||
@@ -31,12 +33,39 @@ class LocalCoverManager(Manager):
|
|||||||
run_after = (SteamAPIManager,)
|
run_after = (SteamAPIManager,)
|
||||||
|
|
||||||
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
def manager_logic(self, game: Game, additional_data: dict) -> None:
|
||||||
# Ensure that the cover path is in the additional data
|
if image_path := additional_data.get("local_image_path"):
|
||||||
try:
|
if not image_path.is_file():
|
||||||
image_path: Path = additional_data["local_image_path"]
|
return
|
||||||
except KeyError:
|
save_cover(game.game_id, resize_cover(image_path))
|
||||||
return
|
elif icon_path := additional_data.get("local_icon_path"):
|
||||||
if not image_path.is_file():
|
cover_width, cover_height = shared.image_size
|
||||||
return
|
|
||||||
# Save the image
|
dest_width = cover_width * 0.7
|
||||||
save_cover(game.game_id, resize_cover(image_path))
|
dest_height = cover_width * 0.7
|
||||||
|
|
||||||
|
dest_x = cover_width * 0.15
|
||||||
|
dest_y = (cover_height - dest_height) / 2
|
||||||
|
|
||||||
|
image = GdkPixbuf.Pixbuf.new_from_file(str(icon_path)).scale_simple(
|
||||||
|
dest_width, dest_height, GdkPixbuf.InterpType.BILINEAR
|
||||||
|
)
|
||||||
|
|
||||||
|
cover = image.scale_simple(
|
||||||
|
1, 2, GdkPixbuf.InterpType.BILINEAR
|
||||||
|
).scale_simple(cover_width, cover_height, GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
|
||||||
|
image.composite(
|
||||||
|
cover,
|
||||||
|
dest_x,
|
||||||
|
dest_y,
|
||||||
|
dest_width,
|
||||||
|
dest_height,
|
||||||
|
dest_x,
|
||||||
|
dest_y,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
GdkPixbuf.InterpType.BILINEAR,
|
||||||
|
255,
|
||||||
|
)
|
||||||
|
|
||||||
|
save_cover(game.game_id, resize_cover(pixbuf=cover))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
from gi.repository import Gio
|
from gi.repository import Gdk, Gio, GLib
|
||||||
from PIL import Image, ImageSequence, UnidentifiedImageError
|
from PIL import Image, ImageSequence, UnidentifiedImageError
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
@@ -63,7 +63,13 @@ def resize_cover(cover_path=None, pixbuf=None):
|
|||||||
else "webp",
|
else "webp",
|
||||||
)
|
)
|
||||||
except UnidentifiedImageError:
|
except UnidentifiedImageError:
|
||||||
return None
|
try:
|
||||||
|
Gdk.Texture.new_from_filename(str(cover_path)).save_to_tiff(
|
||||||
|
tmp_path := Gio.File.new_tmp("XXXXXX.tiff")[0].get_path()
|
||||||
|
)
|
||||||
|
return resize_cover(tmp_path)
|
||||||
|
except GLib.GError:
|
||||||
|
return None
|
||||||
|
|
||||||
return tmp_path
|
return tmp_path
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
hidden_search_entry = Gtk.Template.Child()
|
hidden_search_entry = Gtk.Template.Child()
|
||||||
hidden_search_button = Gtk.Template.Child()
|
hidden_search_button = Gtk.Template.Child()
|
||||||
|
|
||||||
games = {}
|
|
||||||
game_covers = {}
|
game_covers = {}
|
||||||
toasts = {}
|
toasts = {}
|
||||||
active_game = None
|
active_game = None
|
||||||
@@ -100,6 +99,9 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
self.search_entry.connect("search-changed", self.search_changed, False)
|
self.search_entry.connect("search-changed", self.search_changed, False)
|
||||||
self.hidden_search_entry.connect("search-changed", self.search_changed, True)
|
self.hidden_search_entry.connect("search-changed", self.search_changed, True)
|
||||||
|
|
||||||
|
self.search_entry.connect("activate", self.show_details_view_search)
|
||||||
|
self.hidden_search_entry.connect("activate", self.show_details_view_search)
|
||||||
|
|
||||||
back_mouse_button = Gtk.GestureClick(button=8)
|
back_mouse_button = Gtk.GestureClick(button=8)
|
||||||
(back_mouse_button).connect("pressed", self.on_go_back_action)
|
(back_mouse_button).connect("pressed", self.on_go_back_action)
|
||||||
self.add_controller(back_mouse_button)
|
self.add_controller(back_mouse_button)
|
||||||
@@ -115,7 +117,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
def set_library_child(self):
|
def set_library_child(self):
|
||||||
child, hidden_child = self.notice_empty, self.hidden_notice_empty
|
child, hidden_child = self.notice_empty, self.hidden_notice_empty
|
||||||
|
|
||||||
for game in self.games.values():
|
for game in shared.store.games.values():
|
||||||
if game.removed or game.blacklisted:
|
if game.removed or game.blacklisted:
|
||||||
continue
|
continue
|
||||||
if game.hidden:
|
if game.hidden:
|
||||||
@@ -303,13 +305,29 @@ class CartridgesWindow(Adw.ApplicationWindow):
|
|||||||
search_entry.set_text("")
|
search_entry.set_text("")
|
||||||
|
|
||||||
def on_escape_action(self, *_args):
|
def on_escape_action(self, *_args):
|
||||||
if self.stack.get_visible_child() == self.details_view:
|
if (
|
||||||
self.on_go_back_action()
|
|
||||||
elif (
|
|
||||||
self.get_focus() == self.search_entry.get_focus_child()
|
self.get_focus() == self.search_entry.get_focus_child()
|
||||||
or self.hidden_search_entry.get_focus_child()
|
or self.hidden_search_entry.get_focus_child()
|
||||||
):
|
):
|
||||||
self.on_toggle_search_action()
|
self.on_toggle_search_action()
|
||||||
|
else:
|
||||||
|
self.on_go_back_action()
|
||||||
|
|
||||||
|
def show_details_view_search(self, widget):
|
||||||
|
library = (
|
||||||
|
self.hidden_library if widget == self.hidden_search_entry else self.library
|
||||||
|
)
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not (child := library.get_child_at_index(index)):
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.filter_func(child):
|
||||||
|
self.show_details_view(child.get_child())
|
||||||
|
break
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
||||||
def on_undo_action(self, _widget, game=None, undo=None):
|
def on_undo_action(self, _widget, game=None, undo=None):
|
||||||
if not game: # If the action was activated via Ctrl + Z
|
if not game: # If the action was activated via Ctrl + Z
|
||||||
|
|||||||
Reference in New Issue
Block a user