🚧 WIP new location system

TODO
- Locations contain the schema key
- Schema key overriden at location resolve
- No need for callable candidates,
but need to represent "this location's key"
This commit is contained in:
GeoffreyCoulaud
2023-06-19 22:47:56 +02:00
parent 9f582dfa3e
commit f9000be272
11 changed files with 330 additions and 259 deletions

View File

@@ -114,7 +114,7 @@ class Importer:
source, *_rest = data
# Early exit if not installed
if not source.is_installed:
if not source.is_available:
logging.info("Source %s skipped, not installed", source.id)
return
logging.info("Scanning source %s", source.id)

View File

@@ -25,15 +25,12 @@ import yaml
from src import shared
from src.game import Game
from src.importer.sources.location import Location
from src.importer.sources.source import (
SourceIterationResult,
SourceIterator,
URLExecutableSource,
)
from src.utils.decorators import (
replaced_by_path,
replaced_by_schema_key,
)
class BottlesSourceIterator(SourceIterator):
@@ -42,7 +39,7 @@ class BottlesSourceIterator(SourceIterator):
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games"""
data = (self.source.location / "library.yml").read_text("utf-8")
data = self.source.data_location["library.yml"].read_text("utf-8")
library: dict = yaml.safe_load(data)
for entry in library.values():
@@ -65,11 +62,11 @@ class BottlesSourceIterator(SourceIterator):
# as Cartridges can't access directories picked via Bottles' file picker portal
bottles_location = Path(
yaml.safe_load(
(self.source.location / "data.yml").read_text("utf-8")
self.source.data_location["data.yml"].read_text("utf-8")
)["custom_bottles_path"]
)
except (FileNotFoundError, KeyError):
bottles_location = self.source.location / "bottles"
bottles_location = self.source.data_location.root / "bottles"
bottle_path = entry["bottle"]["path"]
image_name = entry["thumbnail"].split(":")[1]
@@ -88,9 +85,14 @@ class BottlesSource(URLExecutableSource):
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
available_on = set(("linux",))
@property
@replaced_by_schema_key
@replaced_by_path("~/.var/app/com.usebottles.bottles/data/bottles/")
@replaced_by_path(shared.data_dir / "bottles")
def location(self) -> Path:
raise FileNotFoundError()
data_location = Location(
candidates=(
lambda: shared.schema.get_string("bottles-location"),
"~/.var/app/com.usebottles.bottles/data/bottles/",
shared.data_dir / "bottles/",
),
paths={
"library.yml": (False, "library.yml"),
"data.yml": (False, "data.yml"),
},
)

View File

@@ -22,21 +22,17 @@ import json
import logging
from hashlib import sha256
from json import JSONDecodeError
from pathlib import Path
from time import time
from typing import Optional, TypedDict
from src import shared
from src.game import Game
from src.importer.sources.location import Location
from src.importer.sources.source import (
URLExecutableSource,
SourceIterationResult,
SourceIterator,
)
from src.utils.decorators import (
replaced_by_path,
replaced_by_schema_key,
)
class HeroicLibraryEntry(TypedDict):
@@ -103,7 +99,7 @@ class HeroicSourceIterator(SourceIterator):
if service == "epic":
uri += "?h=400&resize=1&w=300"
digest = sha256(uri.encode()).hexdigest()
image_path = self.source.location / "images-cache" / digest
image_path = self.source.config_location.root / "images-cache" / digest
additional_data = {"local_image_path": image_path}
return (game, additional_data)
@@ -116,7 +112,7 @@ class HeroicSourceIterator(SourceIterator):
if not shared.schema.get_boolean("heroic-import-" + sub_source["service"]):
continue
# Load games from JSON
file = self.source.location.joinpath(*sub_source["path"])
file = self.source.config_location.root.joinpath(*sub_source["path"])
try:
library = json.load(file.open())["library"]
except (JSONDecodeError, OSError, KeyError):
@@ -141,15 +137,20 @@ class HeroicSource(URLExecutableSource):
url_format = "heroic://launch/{app_name}"
available_on = set(("linux", "win32"))
config_location = Location(
candidates=(
lambda: shared.schema.get_string("heroic-location"),
"~/.var/app/com.heroicgameslauncher.hgl/config/heroic/",
shared.config_dir / "heroic/",
"~/.config/heroic/",
shared.appdata_dir / "heroic/",
),
paths={
"config.json": (False, "config.json"),
},
)
@property
def game_id_format(self) -> str:
"""The string format used to construct game IDs"""
return self.name.lower() + "_{service}_{game_id}"
@property
@replaced_by_schema_key
@replaced_by_path("~/.var/app/com.heroicgameslauncher.hgl/config/heroic/")
@replaced_by_path(shared.config_dir / "heroic")
@replaced_by_path(shared.appdata_dir / "heroic")
def location(self) -> Path:
raise FileNotFoundError()

View File

@@ -18,19 +18,18 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
from shutil import rmtree
from sqlite3 import connect
from time import time
from src import shared
from src.game import Game
from src.importer.sources.location import Location
from src.importer.sources.source import (
SourceIterationResult,
SourceIterator,
URLExecutableSource,
)
from src.utils.decorators import replaced_by_path, replaced_by_schema_key
from src.utils.sqlite import copy_db
@@ -56,7 +55,7 @@ class ItchSourceIterator(SourceIterator):
caves.game_id = games.id
;
"""
db_path = copy_db(self.source.location / "db" / "butler.db")
db_path = copy_db(self.source.config_location["butler.db"])
connection = connect(db_path)
cursor = connection.execute(db_request)
@@ -84,10 +83,13 @@ class ItchSource(URLExecutableSource):
url_format = "itch://caves/{cave_id}/launch"
available_on = set(("linux", "win32"))
@property
@replaced_by_schema_key
@replaced_by_path("~/.var/app/io.itch.itch/config/itch/")
@replaced_by_path(shared.config_dir / "itch")
@replaced_by_path(shared.appdata_dir / "itch")
def location(self) -> Path:
raise FileNotFoundError()
config_location = Location(
candidates=(
lambda: shared.schema.get_string("itch-location"),
"~/.var/app/io.itch.itch/config/itch/",
shared.config_dir / "itch/",
"~/.config/itch/",
shared.appdata_dir / "itch/",
),
paths={"butler.db": (False, "db/butler.db")},
)

View File

@@ -26,8 +26,8 @@ from typing import Generator
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
from src.utils.decorators import replaced_by_path, replaced_by_schema_key
class LegendarySourceIterator(SourceIterator):
@@ -51,7 +51,7 @@ class LegendarySourceIterator(SourceIterator):
data = {}
# Get additional metadata from file (optional)
metadata_file = self.source.location / "metadata" / f"{app_name}.json"
metadata_file = self.source.data_location["metadata"] / f"{app_name}.json"
try:
metadata = json.load(metadata_file.open())
values["developer"] = metadata["metadata"]["developer"]
@@ -67,7 +67,7 @@ class LegendarySourceIterator(SourceIterator):
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
# Open library
file = self.source.location / "installed.json"
file = self.source.data_location["installed.json"]
try:
library: dict = json.load(file.open())
except (JSONDecodeError, OSError):
@@ -89,11 +89,17 @@ class LegendarySourceIterator(SourceIterator):
class LegendarySource(Source):
name = "Legendary"
executable_format = "legendary launch {app_name}"
iterator_class = LegendarySourceIterator
available_on = set(("linux", "win32"))
@property
@replaced_by_schema_key
@replaced_by_path(shared.config_dir / "legendary")
def location(self) -> Path:
raise FileNotFoundError()
iterator_class = LegendarySourceIterator
data_location: Location = Location(
candidates=(
lambda: shared.schema.get_string("legendary-location"),
shared.config_dir / "legendary/",
"~/.config/legendary",
),
paths={
"installed.json": (False, "installed.json"),
"metadata": (True, "metadata"),
},
)

View File

@@ -0,0 +1,64 @@
from pathlib import Path
from typing import Callable, Mapping, Iterable
from os import PathLike
PathSegment = str | PathLike | Path
PathSegments = Iterable[PathSegment]
Candidate = PathSegments | Callable[[], PathSegments]
class UnresolvableLocationError(Exception):
pass
class Location:
"""
Class representing a filesystem location
* A location may have multiple candidate roots
* From its root, multiple subpaths are named and should exist
"""
candidates: Iterable[Candidate]
paths: Mapping[str, tuple[bool, PathSegments]]
root: Path = None
def __init__(
self,
candidates: Iterable[Candidate],
paths: Mapping[str, tuple[bool, PathSegments]],
) -> None:
super().__init__()
self.candidates = candidates
self.paths = paths
def check_candidate(self, candidate: Path) -> bool:
"""Check if a candidate root has the necessary files and directories"""
for type_is_dir, subpath in self.paths.values():
subpath = Path(candidate) / Path(subpath)
if type_is_dir:
if not subpath.is_dir():
return False
else:
if not subpath.is_file():
return False
return True
def resolve(self) -> None:
"""Choose a root path from the candidates for the location.
If none fits, raise a UnresolvableLocationError"""
if self.root is not None:
return
for candidate in self.candidates:
if callable(candidate):
candidate = candidate()
candidate = Path(candidate).expanduser()
if self.check_candidate(candidate):
self.root = candidate
return
raise UnresolvableLocationError()
def __getitem__(self, key: str):
"""Get the computed path from its key for the location"""
self.resolve()
return self.root / self.paths[key][1]

View File

@@ -17,19 +17,18 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from shutil import rmtree
from sqlite3 import connect
from time import time
from src import shared
from src.game import Game
from src.importer.sources.location import Location
from src.importer.sources.source import (
SourceIterationResult,
SourceIterator,
URLExecutableSource,
)
from src.utils.decorators import replaced_by_path, replaced_by_schema_key
from src.utils.sqlite import copy_db
@@ -52,7 +51,7 @@ class LutrisSourceIterator(SourceIterator):
;
"""
params = {"import_steam": shared.schema.get_boolean("lutris-import-steam")}
db_path = copy_db(self.source.location / "pga.db")
db_path = copy_db(self.source.data_location["pga.db"])
connection = connect(db_path)
cursor = connection.execute(request, params)
@@ -73,7 +72,7 @@ class LutrisSourceIterator(SourceIterator):
game = Game(values, allow_side_effects=False)
# Get official image path
image_path = self.source.location / "covers" / "coverart" / f"{row[2]}.jpg"
image_path = self.source.cache_location["coverart"] / f"{row[2]}.jpg"
additional_data = {"local_image_path": image_path}
# Produce game
@@ -91,13 +90,32 @@ class LutrisSource(URLExecutableSource):
url_format = "lutris:rungameid/{game_id}"
available_on = set(("linux",))
# FIXME possible bug: location picks ~/.var... and cache_lcoation picks ~/.local...
data_location = Location(
candidates=(
lambda: shared.schema.get_string("lutris-location"),
"~/.var/app/net.lutris.Lutris/data/lutris/",
shared.data_dir / "lutris/",
"~/.local/share/lutris/",
),
paths={
"pga.db": (False, "pga.db"),
},
)
cache_location = Location(
candidates=(
lambda: shared.schema.get_string("lutris-cache-location"),
"~/.var/app/net.lutris.Lutris/cache/lutris/",
shared.cache_dir / "lutris/",
"~/.cache/lutris",
),
paths={
"coverart": (True, "coverart"),
},
)
@property
def game_id_format(self):
return super().game_id_format + "_{game_internal_id}"
@property
@replaced_by_schema_key
@replaced_by_path("~/.var/app/net.lutris.Lutris/data/lutris/")
@replaced_by_path("~/.local/share/lutris/")
def location(self):
raise FileNotFoundError()

View File

@@ -20,10 +20,9 @@
import sys
from abc import abstractmethod
from collections.abc import Iterable, Iterator
from pathlib import Path
from typing import Generator, Any
from typing import Generator, Any, Optional
from src import shared
from src.importer.sources.location import Location
from src.game import Game
# Type of the data returned by iterating on a Source
@@ -62,9 +61,12 @@ class Source(Iterable):
"""Source of games. E.g an installed app with a config file that lists game directories"""
name: str
iterator_class: type[SourceIterator]
variant: str = None
available_on: set[str] = set()
data_location: Optional[Location] = None
cache_location: Optional[Location] = None
config_location: Optional[Location] = None
iterator_class: type[SourceIterator]
@property
def full_name(self) -> str:
@@ -88,44 +90,21 @@ class Source(Iterable):
return self.name.lower() + "_{game_id}"
@property
def is_installed(self):
# pylint: disable=pointless-statement
try:
self.location
except FileNotFoundError:
return False
def is_available(self):
return sys.platform in self.available_on
@property
def location_key(self) -> str:
"""
The schema key pointing to the user-set location for the source.
May be overriden by inherinting classes.
"""
return f"{self.name.lower()}-location"
def update_location_schema_key(self):
"""Update the schema value for this source's location if possible"""
try:
location = self.location
except FileNotFoundError:
return
shared.schema.set_string(self.location_key, location)
def __iter__(self) -> SourceIterator:
"""Get an iterator for the source"""
return self.iterator_class(self)
@property
@abstractmethod
def location(self) -> Path:
"""The source's location on disk"""
@property
@abstractmethod
def executable_format(self) -> str:
"""The executable format used to construct game executables"""
def __iter__(self) -> SourceIterator:
"""Get an iterator for the source"""
for location in (self.data_location, self.cache_location, self.config_location):
if location is not None:
location.resolve()
return self.iterator_class(self)
# pylint: disable=abstract-method
class URLExecutableSource(Source):

View File

@@ -35,6 +35,7 @@ from src.utils.decorators import (
replaced_by_schema_key,
)
from src.utils.steam import SteamFileHelper, SteamInvalidManifestError
from src.importer.sources.location import Location
class SteamSourceIterator(SourceIterator):
@@ -42,7 +43,7 @@ class SteamSourceIterator(SourceIterator):
def get_manifest_dirs(self) -> Iterable[Path]:
"""Get dirs that contain steam app manifests"""
libraryfolders_path = self.source.location / "steamapps" / "libraryfolders.vdf"
libraryfolders_path = self.source.data_location["libraryfolders.vdf"]
with open(libraryfolders_path, "r", encoding="utf-8") as file:
contents = file.read()
return [
@@ -101,9 +102,7 @@ class SteamSourceIterator(SourceIterator):
# Add official cover image
image_path = (
self.source.location
/ "appcache"
/ "librarycache"
self.source.data_location["librarycache"]
/ f"{appid}_library_600x900.jpg"
)
additional_data = {"local_image_path": image_path, "steam_appid": appid}
@@ -114,15 +113,20 @@ class SteamSourceIterator(SourceIterator):
class SteamSource(URLExecutableSource):
name = "Steam"
available_on = set(("linux", "win32"))
iterator_class = SteamSourceIterator
url_format = "steam://rungameid/{game_id}"
available_on = set(("linux", "win32"))
@property
@replaced_by_schema_key
@replaced_by_path("~/.var/app/com.valvesoftware.Steam/data/Steam/")
@replaced_by_path(shared.data_dir / "Steam")
@replaced_by_path("~/.steam/")
@replaced_by_path(shared.programfiles32_dir / "Steam")
def location(self):
raise FileNotFoundError()
data_location = Location(
candidates=(
lambda: shared.schema.get_string("steam-location"),
"~/.var/app/com.valvesoftware.Steam/data/Steam/",
shared.data_dir / "Steam/",
"~/.steam/",
shared.programfiles32_dir / "Steam",
),
paths={
"libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"),
"librarycache": (True, "appcache/librarycache"),
},
)

View File

@@ -24,15 +24,14 @@ from shutil import rmtree
from gi.repository import Adw, Gio, GLib, Gtk
# pylint: disable=unused-import
from src import shared
# TODO use the new sources
from src.importers.bottles_importer import bottles_installed
from src.importers.heroic_importer import heroic_installed
from src.importers.itch_importer import itch_installed
from src.importers.lutris_importer import lutris_cache_exists, lutris_installed
from src.importers.steam_importer import steam_installed
from src.importer.sources.bottles_source import BottlesSource
from src.importer.sources.heroic_source import HeroicSource
from src.importer.sources.itch_source import ItchSource
from src.importer.sources.legendary_source import LegendarySource
from src.importer.sources.lutris_source import LutrisSource
from src.importer.sources.source import Source
from src.importer.sources.steam_source import SteamSource
from src.utils.create_dialog import create_dialog
@@ -49,37 +48,36 @@ class PreferencesWindow(Adw.PreferencesWindow):
exit_after_launch_switch = Gtk.Template.Child()
cover_launches_game_switch = Gtk.Template.Child()
high_quality_images_switch = Gtk.Template.Child()
remove_all_games_button = Gtk.Template.Child()
steam_expander_row = Gtk.Template.Child()
steam_action_row = Gtk.Template.Child()
steam_file_chooser_button = Gtk.Template.Child()
steam_data_action_row = Gtk.Template.Child()
steam_data_file_chooser_button = Gtk.Template.Child()
lutris_expander_row = Gtk.Template.Child()
lutris_action_row = Gtk.Template.Child()
lutris_file_chooser_button = Gtk.Template.Child()
lutris_data_action_row = Gtk.Template.Child()
lutris_data_file_chooser_button = Gtk.Template.Child()
lutris_cache_action_row = Gtk.Template.Child()
lutris_cache_file_chooser_button = Gtk.Template.Child()
lutris_import_steam_switch = Gtk.Template.Child()
heroic_expander_row = Gtk.Template.Child()
heroic_action_row = Gtk.Template.Child()
heroic_file_chooser_button = Gtk.Template.Child()
heroic_config_action_row = Gtk.Template.Child()
heroic_config_file_chooser_button = Gtk.Template.Child()
heroic_import_epic_switch = Gtk.Template.Child()
heroic_import_gog_switch = Gtk.Template.Child()
heroic_import_sideload_switch = Gtk.Template.Child()
bottles_expander_row = Gtk.Template.Child()
bottles_action_row = Gtk.Template.Child()
bottles_file_chooser_button = Gtk.Template.Child()
bottles_data_action_row = Gtk.Template.Child()
bottles_data_file_chooser_button = Gtk.Template.Child()
itch_expander_row = Gtk.Template.Child()
itch_action_row = Gtk.Template.Child()
itch_file_chooser_button = Gtk.Template.Child()
itch_config_action_row = Gtk.Template.Child()
itch_config_file_chooser_button = Gtk.Template.Child()
legendary_expander_row = Gtk.Template.Child()
legendary_action_row = Gtk.Template.Child()
legendary_file_chooser_button = Gtk.Template.Child()
legendary_config_action_row = Gtk.Template.Child()
legendary_config_file_chooser_button = Gtk.Template.Child()
sgdb_key_group = Gtk.Template.Child()
sgdb_key_entry_row = Gtk.Template.Child()
@@ -89,6 +87,9 @@ class PreferencesWindow(Adw.PreferencesWindow):
sgdb_animated_switch = Gtk.Template.Child()
danger_zone_group = Gtk.Template.Child()
reset_action_row = Gtk.Template.Child()
reset_button = Gtk.Template.Child()
remove_all_games_button = Gtk.Template.Child()
removed_games = set()
@@ -116,87 +117,25 @@ class PreferencesWindow(Adw.PreferencesWindow):
# Debug
if shared.PROFILE == "development":
def reset_app(*_args):
rmtree(shared.data_dir / "cartridges", True)
rmtree(shared.config_dir / "cartridges", True)
rmtree(shared.cache_dir / "cartridges", True)
for key in (
(settings_schema_source := Gio.SettingsSchemaSource.get_default())
.lookup(shared.APP_ID, True)
.list_keys()
):
shared.schema.reset(key)
for key in settings_schema_source.lookup(
shared.APP_ID + ".State", True
).list_keys():
shared.state_schema.reset(key)
shared.win.get_application().quit()
reset_button = Gtk.Button.new_with_label("Reset")
reset_button.set_valign(Gtk.Align.CENTER)
reset_button.add_css_class("destructive-action")
reset_button.connect("clicked", reset_app)
self.danger_zone_group.add(
(
reset_action_row := Adw.ActionRow(
title="Reset App",
subtitle="Completely resets and quits Cartridges",
)
)
)
reset_action_row.add_suffix(reset_button)
self.reset_action_row.set_visible(True)
self.reset_button.connect("clicked", self.reset_app)
self.set_default_size(-1, 560)
# Steam
self.create_preferences(self, "steam", "Steam")
# Lutris
self.create_preferences(self, "lutris", "Lutris")
def set_cache_dir(_source, result, *_args):
try:
path = Path(self.file_chooser.select_folder_finish(result).get_path())
except GLib.GError:
return
def response(widget, response):
if response == "choose_folder":
self.choose_folder(widget, set_cache_dir)
if lutris_cache_exists(path):
self.set_subtitle(self, "lutris-cache")
# Sources settings
for source_class in (
BottlesSource,
HeroicSource,
ItchSource,
LegendarySource,
LutrisSource,
SteamSource,
):
source = source_class()
if not source.is_available:
expander_row = getattr(self, f"{source.id}_expander_row")
expander_row.remove()
else:
create_dialog(
self.win,
_("Cache Not Found"),
_("Select the Lutris cache directory."),
"choose_folder",
_("Set Location"),
).connect("response", response)
self.set_subtitle(self, "lutris-cache")
self.lutris_cache_file_chooser_button.connect(
"clicked", self.choose_folder, set_cache_dir
)
# Heroic
self.create_preferences(self, "heroic", "Heroic", True)
# Bottles
self.create_preferences(self, "bottles", "Bottles")
# itch
self.create_preferences(self, "itch", "itch", True)
# Legendary
self.create_preferences(self, "legendary", "Legendary", True)
self.init_source_row(source)
# SteamGridDB
def sgdb_key_changed(*_args):
@@ -238,11 +177,6 @@ class PreferencesWindow(Adw.PreferencesWindow):
)
)
# Windows
if os.name == "nt":
self.sources_group.remove(self.lutris_expander_row)
self.sources_group.remove(self.bottles_expander_row)
def get_switch(self, setting):
return getattr(self, f'{setting.replace("-", "_")}_switch')
@@ -255,8 +189,8 @@ class PreferencesWindow(Adw.PreferencesWindow):
Gio.SettingsBindFlags.DEFAULT,
)
def choose_folder(self, _widget, function):
self.file_chooser.select_folder(self.win, None, function, None)
def choose_folder(self, _widget, callback, callback_data=None):
self.file_chooser.select_folder(self.win, None, callback, callback_data)
def undo_remove_all(self, *_args):
for game in self.removed_games:
@@ -281,53 +215,98 @@ class PreferencesWindow(Adw.PreferencesWindow):
self.add_toast(self.toast)
def set_subtitle(self, win, source_id):
getattr(win, f'{source_id.replace("-", "_")}_action_row').set_subtitle(
# Remove the path if the dir is picked via the Flatpak portal
re.sub(
"/run/user/\\d*/doc/.*/",
"",
str(
Path(shared.schema.get_string(f"{source_id}-location")).expanduser()
),
)
)
def reset_app(*_args):
rmtree(shared.data_dir / "cartridges", True)
rmtree(shared.config_dir / "cartridges", True)
rmtree(shared.cache_dir / "cartridges", True)
for key in (
(settings_schema_source := Gio.SettingsSchemaSource.get_default())
.lookup(shared.APP_ID, True)
.list_keys()
):
shared.schema.reset(key)
for key in settings_schema_source.lookup(
shared.APP_ID + ".State", True
).list_keys():
shared.state_schema.reset(key)
shared.win.get_application().quit()
def update_source_action_row_paths(self, source):
"""Set the dir subtitle for a source's action rows"""
for location in ("data", "config", "cache"):
# Get the action row to subtitle
action_row = getattr(self, f"{source.id}_{location}_action_row", None)
if not action_row:
continue
# Historically "location" meant data or config, so the key stays shared
infix = "-cache" if location == "cache" else ""
key = f"{source.id}{infix}-location"
path = Path(shared.schema.get_string(key)).expanduser()
# Remove the path if the dir is picked via the Flatpak portal
subtitle = re.sub("/run/user/\\d*/doc/.*/", "", str(path))
action_row.set_subtitle(subtitle)
def init_source_row(self, source: Source):
"""Initialize a preference row for a source class"""
def set_dir(_widget, result, location_name):
"""Callback called when a dir picker button is clicked"""
def create_preferences(self, win, source_id, name, config=False):
def set_dir(_source, result, *_args):
try:
path = Path(win.file_chooser.select_folder_finish(result).get_path())
path = Path(self.file_chooser.select_folder_finish(result).get_path())
except GLib.GError:
return
def response(widget, response):
if response == "choose_folder":
win.choose_folder(widget, set_dir)
if globals()[f"{source_id}_installed"](path):
self.set_subtitle(win, source_id)
# Good picked location
location = getattr(source, f"{location_name}_location")
if location.check_candidate(path):
# Set the schema
infix = "-cache" if location == "cache" else ""
key = f"{source.id}{infix}-location"
shared.schema.set_string(key, str(path))
# Update the row
self.update_source_action_row_paths(source)
# Bad picked location, inform user
else:
create_dialog(
win,
_("Installation Not Found"),
# The variable is the name of the game launcher
_("Select the {} configuration directory.").format(name) if config
# The variable is the name of the game launcher
else _("Select the {} data directory.").format(name),
if location_name == "cache":
title = "Cache not found"
subtitle_format = "Select the {} cache directory."
else:
title = "Installation not found"
subtitle_format = "Select the {} installation directory."
dialog = create_dialog(
self,
_(title),
_(subtitle_format).format(source.name),
"choose_folder",
_("Set Location"),
).connect("response", response)
)
self.set_subtitle(win, source_id)
def on_response(widget, response):
if response == "choose_folder":
self.choose_folder(widget, set_dir, location_name)
dialog.connect("response", on_response)
# Bind expander row activation to source being enabled
expander_row = getattr(self, f"{source.id}_expander_row")
shared.schema.bind(
source_id,
getattr(win, f"{source_id}_expander_row"),
source.id,
expander_row,
"enable-expansion",
Gio.SettingsBindFlags.DEFAULT,
)
getattr(win, f"{source_id}_file_chooser_button").connect(
"clicked", win.choose_folder, set_dir
)
# Connect dir picker buttons
for location in ("data", "config", "cache"):
button = getattr(self, f"{source.id}_{location}_file_chooser_button", None)
if button is not None:
button.connect("clicked", self.choose_folder, set_dir, location)
# Set the source row subtitles
self.update_source_action_row_paths(source)