🚧 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:
@@ -61,6 +61,22 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Adw.ActionRow reset_action_row {
|
||||||
|
title: _("Reset App");
|
||||||
|
subtitle: _("Completely resets and quits Cartridges");
|
||||||
|
visible: false;
|
||||||
|
|
||||||
|
Button reset_button {
|
||||||
|
label: _("Reset");
|
||||||
|
valign: center;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"destructive-action",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,10 +92,10 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
title: _("Steam");
|
title: _("Steam");
|
||||||
show-enable-switch: true;
|
show-enable-switch: true;
|
||||||
|
|
||||||
Adw.ActionRow steam_action_row {
|
Adw.ActionRow steam_data_action_row {
|
||||||
title: _("Install Location");
|
title: _("Install Location");
|
||||||
|
|
||||||
Button steam_file_chooser_button {
|
Button steam_data_file_chooser_button {
|
||||||
icon-name: "folder-symbolic";
|
icon-name: "folder-symbolic";
|
||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
@@ -90,10 +106,10 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
title: _("Lutris");
|
title: _("Lutris");
|
||||||
show-enable-switch: true;
|
show-enable-switch: true;
|
||||||
|
|
||||||
Adw.ActionRow lutris_action_row {
|
Adw.ActionRow lutris_data_action_row {
|
||||||
title: _("Install Location");
|
title: _("Install Location");
|
||||||
|
|
||||||
Button lutris_file_chooser_button {
|
Button lutris_data_file_chooser_button {
|
||||||
icon-name: "folder-symbolic";
|
icon-name: "folder-symbolic";
|
||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
@@ -122,10 +138,10 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
title: _("Heroic");
|
title: _("Heroic");
|
||||||
show-enable-switch: true;
|
show-enable-switch: true;
|
||||||
|
|
||||||
Adw.ActionRow heroic_action_row {
|
Adw.ActionRow heroic_config_action_row {
|
||||||
title: _("Install Location");
|
title: _("Install Location");
|
||||||
|
|
||||||
Button heroic_file_chooser_button {
|
Button heroic_config_file_chooser_button {
|
||||||
icon-name: "folder-symbolic";
|
icon-name: "folder-symbolic";
|
||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
@@ -163,10 +179,10 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
title: _("Bottles");
|
title: _("Bottles");
|
||||||
show-enable-switch: true;
|
show-enable-switch: true;
|
||||||
|
|
||||||
Adw.ActionRow bottles_action_row {
|
Adw.ActionRow bottles_data_action_row {
|
||||||
title: _("Install Location");
|
title: _("Install Location");
|
||||||
|
|
||||||
Button bottles_file_chooser_button {
|
Button bottles_data_file_chooser_button {
|
||||||
icon-name: "folder-symbolic";
|
icon-name: "folder-symbolic";
|
||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
@@ -177,10 +193,10 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
title: _("itch");
|
title: _("itch");
|
||||||
show-enable-switch: true;
|
show-enable-switch: true;
|
||||||
|
|
||||||
Adw.ActionRow itch_action_row {
|
Adw.ActionRow itch_config_action_row {
|
||||||
title: _("Install Location");
|
title: _("Install Location");
|
||||||
|
|
||||||
Button itch_file_chooser_button {
|
Button itch_config_file_chooser_button {
|
||||||
icon-name: "folder-symbolic";
|
icon-name: "folder-symbolic";
|
||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
@@ -191,10 +207,10 @@ template $PreferencesWindow : Adw.PreferencesWindow {
|
|||||||
title: _("Legendary");
|
title: _("Legendary");
|
||||||
show-enable-switch: true;
|
show-enable-switch: true;
|
||||||
|
|
||||||
Adw.ActionRow legendary_action_row {
|
Adw.ActionRow legendary_config_action_row {
|
||||||
title: _("Install Location");
|
title: _("Install Location");
|
||||||
|
|
||||||
Button legendary_file_chooser_button {
|
Button legendary_config_file_chooser_button {
|
||||||
icon-name: "folder-symbolic";
|
icon-name: "folder-symbolic";
|
||||||
valign: center;
|
valign: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class Importer:
|
|||||||
source, *_rest = data
|
source, *_rest = data
|
||||||
|
|
||||||
# Early exit if not installed
|
# Early exit if not installed
|
||||||
if not source.is_installed:
|
if not source.is_available:
|
||||||
logging.info("Source %s skipped, not installed", source.id)
|
logging.info("Source %s skipped, not installed", source.id)
|
||||||
return
|
return
|
||||||
logging.info("Scanning source %s", source.id)
|
logging.info("Scanning source %s", source.id)
|
||||||
|
|||||||
@@ -25,15 +25,12 @@ import yaml
|
|||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location
|
||||||
from src.importer.sources.source import (
|
from src.importer.sources.source import (
|
||||||
SourceIterationResult,
|
SourceIterationResult,
|
||||||
SourceIterator,
|
SourceIterator,
|
||||||
URLExecutableSource,
|
URLExecutableSource,
|
||||||
)
|
)
|
||||||
from src.utils.decorators import (
|
|
||||||
replaced_by_path,
|
|
||||||
replaced_by_schema_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BottlesSourceIterator(SourceIterator):
|
class BottlesSourceIterator(SourceIterator):
|
||||||
@@ -42,7 +39,7 @@ class BottlesSourceIterator(SourceIterator):
|
|||||||
def generator_builder(self) -> SourceIterationResult:
|
def generator_builder(self) -> SourceIterationResult:
|
||||||
"""Generator method producing games"""
|
"""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)
|
library: dict = yaml.safe_load(data)
|
||||||
|
|
||||||
for entry in library.values():
|
for entry in library.values():
|
||||||
@@ -65,11 +62,11 @@ class BottlesSourceIterator(SourceIterator):
|
|||||||
# as Cartridges can't access directories picked via Bottles' file picker portal
|
# as Cartridges can't access directories picked via Bottles' file picker portal
|
||||||
bottles_location = Path(
|
bottles_location = Path(
|
||||||
yaml.safe_load(
|
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"]
|
)["custom_bottles_path"]
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, KeyError):
|
except (FileNotFoundError, KeyError):
|
||||||
bottles_location = self.source.location / "bottles"
|
bottles_location = self.source.data_location.root / "bottles"
|
||||||
|
|
||||||
bottle_path = entry["bottle"]["path"]
|
bottle_path = entry["bottle"]["path"]
|
||||||
image_name = entry["thumbnail"].split(":")[1]
|
image_name = entry["thumbnail"].split(":")[1]
|
||||||
@@ -88,9 +85,14 @@ class BottlesSource(URLExecutableSource):
|
|||||||
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
|
url_format = 'bottles:run/"{bottle_name}"/"{game_name}"'
|
||||||
available_on = set(("linux",))
|
available_on = set(("linux",))
|
||||||
|
|
||||||
@property
|
data_location = Location(
|
||||||
@replaced_by_schema_key
|
candidates=(
|
||||||
@replaced_by_path("~/.var/app/com.usebottles.bottles/data/bottles/")
|
lambda: shared.schema.get_string("bottles-location"),
|
||||||
@replaced_by_path(shared.data_dir / "bottles")
|
"~/.var/app/com.usebottles.bottles/data/bottles/",
|
||||||
def location(self) -> Path:
|
shared.data_dir / "bottles/",
|
||||||
raise FileNotFoundError()
|
),
|
||||||
|
paths={
|
||||||
|
"library.yml": (False, "library.yml"),
|
||||||
|
"data.yml": (False, "data.yml"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -22,21 +22,17 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from pathlib import Path
|
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Optional, TypedDict
|
from typing import Optional, TypedDict
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location
|
||||||
from src.importer.sources.source import (
|
from src.importer.sources.source import (
|
||||||
URLExecutableSource,
|
URLExecutableSource,
|
||||||
SourceIterationResult,
|
SourceIterationResult,
|
||||||
SourceIterator,
|
SourceIterator,
|
||||||
)
|
)
|
||||||
from src.utils.decorators import (
|
|
||||||
replaced_by_path,
|
|
||||||
replaced_by_schema_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HeroicLibraryEntry(TypedDict):
|
class HeroicLibraryEntry(TypedDict):
|
||||||
@@ -103,7 +99,7 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
if service == "epic":
|
if service == "epic":
|
||||||
uri += "?h=400&resize=1&w=300"
|
uri += "?h=400&resize=1&w=300"
|
||||||
digest = sha256(uri.encode()).hexdigest()
|
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}
|
additional_data = {"local_image_path": image_path}
|
||||||
|
|
||||||
return (game, additional_data)
|
return (game, additional_data)
|
||||||
@@ -116,7 +112,7 @@ class HeroicSourceIterator(SourceIterator):
|
|||||||
if not shared.schema.get_boolean("heroic-import-" + sub_source["service"]):
|
if not shared.schema.get_boolean("heroic-import-" + sub_source["service"]):
|
||||||
continue
|
continue
|
||||||
# Load games from JSON
|
# Load games from JSON
|
||||||
file = self.source.location.joinpath(*sub_source["path"])
|
file = self.source.config_location.root.joinpath(*sub_source["path"])
|
||||||
try:
|
try:
|
||||||
library = json.load(file.open())["library"]
|
library = json.load(file.open())["library"]
|
||||||
except (JSONDecodeError, OSError, KeyError):
|
except (JSONDecodeError, OSError, KeyError):
|
||||||
@@ -141,15 +137,20 @@ class HeroicSource(URLExecutableSource):
|
|||||||
url_format = "heroic://launch/{app_name}"
|
url_format = "heroic://launch/{app_name}"
|
||||||
available_on = set(("linux", "win32"))
|
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
|
@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() + "_{service}_{game_id}"
|
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()
|
|
||||||
|
|||||||
@@ -18,19 +18,18 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from sqlite3 import connect
|
from sqlite3 import connect
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location
|
||||||
from src.importer.sources.source import (
|
from src.importer.sources.source import (
|
||||||
SourceIterationResult,
|
SourceIterationResult,
|
||||||
SourceIterator,
|
SourceIterator,
|
||||||
URLExecutableSource,
|
URLExecutableSource,
|
||||||
)
|
)
|
||||||
from src.utils.decorators import replaced_by_path, replaced_by_schema_key
|
|
||||||
from src.utils.sqlite import copy_db
|
from src.utils.sqlite import copy_db
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ class ItchSourceIterator(SourceIterator):
|
|||||||
caves.game_id = games.id
|
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)
|
connection = connect(db_path)
|
||||||
cursor = connection.execute(db_request)
|
cursor = connection.execute(db_request)
|
||||||
|
|
||||||
@@ -84,10 +83,13 @@ class ItchSource(URLExecutableSource):
|
|||||||
url_format = "itch://caves/{cave_id}/launch"
|
url_format = "itch://caves/{cave_id}/launch"
|
||||||
available_on = set(("linux", "win32"))
|
available_on = set(("linux", "win32"))
|
||||||
|
|
||||||
@property
|
config_location = Location(
|
||||||
@replaced_by_schema_key
|
candidates=(
|
||||||
@replaced_by_path("~/.var/app/io.itch.itch/config/itch/")
|
lambda: shared.schema.get_string("itch-location"),
|
||||||
@replaced_by_path(shared.config_dir / "itch")
|
"~/.var/app/io.itch.itch/config/itch/",
|
||||||
@replaced_by_path(shared.appdata_dir / "itch")
|
shared.config_dir / "itch/",
|
||||||
def location(self) -> Path:
|
"~/.config/itch/",
|
||||||
raise FileNotFoundError()
|
shared.appdata_dir / "itch/",
|
||||||
|
),
|
||||||
|
paths={"butler.db": (False, "db/butler.db")},
|
||||||
|
)
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ from typing import Generator
|
|||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location
|
||||||
from src.importer.sources.source import Source, SourceIterationResult, SourceIterator
|
from src.importer.sources.source import Source, SourceIterationResult, SourceIterator
|
||||||
from src.utils.decorators import replaced_by_path, replaced_by_schema_key
|
|
||||||
|
|
||||||
|
|
||||||
class LegendarySourceIterator(SourceIterator):
|
class LegendarySourceIterator(SourceIterator):
|
||||||
@@ -51,7 +51,7 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
# Get additional metadata from file (optional)
|
# 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:
|
try:
|
||||||
metadata = json.load(metadata_file.open())
|
metadata = json.load(metadata_file.open())
|
||||||
values["developer"] = metadata["metadata"]["developer"]
|
values["developer"] = metadata["metadata"]["developer"]
|
||||||
@@ -67,7 +67,7 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
|
|
||||||
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
|
||||||
# Open library
|
# Open library
|
||||||
file = self.source.location / "installed.json"
|
file = self.source.data_location["installed.json"]
|
||||||
try:
|
try:
|
||||||
library: dict = json.load(file.open())
|
library: dict = json.load(file.open())
|
||||||
except (JSONDecodeError, OSError):
|
except (JSONDecodeError, OSError):
|
||||||
@@ -89,11 +89,17 @@ class LegendarySourceIterator(SourceIterator):
|
|||||||
class LegendarySource(Source):
|
class LegendarySource(Source):
|
||||||
name = "Legendary"
|
name = "Legendary"
|
||||||
executable_format = "legendary launch {app_name}"
|
executable_format = "legendary launch {app_name}"
|
||||||
iterator_class = LegendarySourceIterator
|
|
||||||
available_on = set(("linux", "win32"))
|
available_on = set(("linux", "win32"))
|
||||||
|
|
||||||
@property
|
iterator_class = LegendarySourceIterator
|
||||||
@replaced_by_schema_key
|
data_location: Location = Location(
|
||||||
@replaced_by_path(shared.config_dir / "legendary")
|
candidates=(
|
||||||
def location(self) -> Path:
|
lambda: shared.schema.get_string("legendary-location"),
|
||||||
raise FileNotFoundError()
|
shared.config_dir / "legendary/",
|
||||||
|
"~/.config/legendary",
|
||||||
|
),
|
||||||
|
paths={
|
||||||
|
"installed.json": (False, "installed.json"),
|
||||||
|
"metadata": (True, "metadata"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
64
src/importer/sources/location.py
Normal file
64
src/importer/sources/location.py
Normal 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]
|
||||||
@@ -17,19 +17,18 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from sqlite3 import connect
|
from sqlite3 import connect
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from src import shared
|
from src import shared
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
from src.importer.sources.location import Location
|
||||||
from src.importer.sources.source import (
|
from src.importer.sources.source import (
|
||||||
SourceIterationResult,
|
SourceIterationResult,
|
||||||
SourceIterator,
|
SourceIterator,
|
||||||
URLExecutableSource,
|
URLExecutableSource,
|
||||||
)
|
)
|
||||||
from src.utils.decorators import replaced_by_path, replaced_by_schema_key
|
|
||||||
from src.utils.sqlite import copy_db
|
from src.utils.sqlite import copy_db
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
;
|
;
|
||||||
"""
|
"""
|
||||||
params = {"import_steam": shared.schema.get_boolean("lutris-import-steam")}
|
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)
|
connection = connect(db_path)
|
||||||
cursor = connection.execute(request, params)
|
cursor = connection.execute(request, params)
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ class LutrisSourceIterator(SourceIterator):
|
|||||||
game = Game(values, allow_side_effects=False)
|
game = Game(values, allow_side_effects=False)
|
||||||
|
|
||||||
# Get official image path
|
# 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}
|
additional_data = {"local_image_path": image_path}
|
||||||
|
|
||||||
# Produce game
|
# Produce game
|
||||||
@@ -91,13 +90,32 @@ class LutrisSource(URLExecutableSource):
|
|||||||
url_format = "lutris:rungameid/{game_id}"
|
url_format = "lutris:rungameid/{game_id}"
|
||||||
available_on = set(("linux",))
|
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
|
@property
|
||||||
def game_id_format(self):
|
def game_id_format(self):
|
||||||
return super().game_id_format + "_{game_internal_id}"
|
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()
|
|
||||||
|
|||||||
@@ -20,10 +20,9 @@
|
|||||||
import sys
|
import sys
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from pathlib import Path
|
from typing import Generator, Any, Optional
|
||||||
from typing import Generator, Any
|
|
||||||
|
|
||||||
from src import shared
|
from src.importer.sources.location import Location
|
||||||
from src.game import Game
|
from src.game import Game
|
||||||
|
|
||||||
# Type of the data returned by iterating on a Source
|
# 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"""
|
"""Source of games. E.g an installed app with a config file that lists game directories"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
iterator_class: type[SourceIterator]
|
|
||||||
variant: str = None
|
variant: str = None
|
||||||
available_on: set[str] = set()
|
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
|
@property
|
||||||
def full_name(self) -> str:
|
def full_name(self) -> str:
|
||||||
@@ -88,44 +90,21 @@ class Source(Iterable):
|
|||||||
return self.name.lower() + "_{game_id}"
|
return self.name.lower() + "_{game_id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_installed(self):
|
def is_available(self):
|
||||||
# pylint: disable=pointless-statement
|
|
||||||
try:
|
|
||||||
self.location
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
return sys.platform in self.available_on
|
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
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def executable_format(self) -> str:
|
def executable_format(self) -> str:
|
||||||
"""The executable format used to construct game executables"""
|
"""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
|
# pylint: disable=abstract-method
|
||||||
class URLExecutableSource(Source):
|
class URLExecutableSource(Source):
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from src.utils.decorators import (
|
|||||||
replaced_by_schema_key,
|
replaced_by_schema_key,
|
||||||
)
|
)
|
||||||
from src.utils.steam import SteamFileHelper, SteamInvalidManifestError
|
from src.utils.steam import SteamFileHelper, SteamInvalidManifestError
|
||||||
|
from src.importer.sources.location import Location
|
||||||
|
|
||||||
|
|
||||||
class SteamSourceIterator(SourceIterator):
|
class SteamSourceIterator(SourceIterator):
|
||||||
@@ -42,7 +43,7 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
def get_manifest_dirs(self) -> Iterable[Path]:
|
def get_manifest_dirs(self) -> Iterable[Path]:
|
||||||
"""Get dirs that contain steam app manifests"""
|
"""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:
|
with open(libraryfolders_path, "r", encoding="utf-8") as file:
|
||||||
contents = file.read()
|
contents = file.read()
|
||||||
return [
|
return [
|
||||||
@@ -101,9 +102,7 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
# Add official cover image
|
# Add official cover image
|
||||||
image_path = (
|
image_path = (
|
||||||
self.source.location
|
self.source.data_location["librarycache"]
|
||||||
/ "appcache"
|
|
||||||
/ "librarycache"
|
|
||||||
/ f"{appid}_library_600x900.jpg"
|
/ f"{appid}_library_600x900.jpg"
|
||||||
)
|
)
|
||||||
additional_data = {"local_image_path": image_path, "steam_appid": appid}
|
additional_data = {"local_image_path": image_path, "steam_appid": appid}
|
||||||
@@ -114,15 +113,20 @@ class SteamSourceIterator(SourceIterator):
|
|||||||
|
|
||||||
class SteamSource(URLExecutableSource):
|
class SteamSource(URLExecutableSource):
|
||||||
name = "Steam"
|
name = "Steam"
|
||||||
|
available_on = set(("linux", "win32"))
|
||||||
iterator_class = SteamSourceIterator
|
iterator_class = SteamSourceIterator
|
||||||
url_format = "steam://rungameid/{game_id}"
|
url_format = "steam://rungameid/{game_id}"
|
||||||
available_on = set(("linux", "win32"))
|
|
||||||
|
|
||||||
@property
|
data_location = Location(
|
||||||
@replaced_by_schema_key
|
candidates=(
|
||||||
@replaced_by_path("~/.var/app/com.valvesoftware.Steam/data/Steam/")
|
lambda: shared.schema.get_string("steam-location"),
|
||||||
@replaced_by_path(shared.data_dir / "Steam")
|
"~/.var/app/com.valvesoftware.Steam/data/Steam/",
|
||||||
@replaced_by_path("~/.steam/")
|
shared.data_dir / "Steam/",
|
||||||
@replaced_by_path(shared.programfiles32_dir / "Steam")
|
"~/.steam/",
|
||||||
def location(self):
|
shared.programfiles32_dir / "Steam",
|
||||||
raise FileNotFoundError()
|
),
|
||||||
|
paths={
|
||||||
|
"libraryfolders.vdf": (False, "steamapps/libraryfolders.vdf"),
|
||||||
|
"librarycache": (True, "appcache/librarycache"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -24,15 +24,14 @@ from shutil import rmtree
|
|||||||
|
|
||||||
from gi.repository import Adw, Gio, GLib, Gtk
|
from gi.repository import Adw, Gio, GLib, Gtk
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
|
||||||
from src import shared
|
from src import shared
|
||||||
|
from src.importer.sources.bottles_source import BottlesSource
|
||||||
# TODO use the new sources
|
from src.importer.sources.heroic_source import HeroicSource
|
||||||
from src.importers.bottles_importer import bottles_installed
|
from src.importer.sources.itch_source import ItchSource
|
||||||
from src.importers.heroic_importer import heroic_installed
|
from src.importer.sources.legendary_source import LegendarySource
|
||||||
from src.importers.itch_importer import itch_installed
|
from src.importer.sources.lutris_source import LutrisSource
|
||||||
from src.importers.lutris_importer import lutris_cache_exists, lutris_installed
|
from src.importer.sources.source import Source
|
||||||
from src.importers.steam_importer import steam_installed
|
from src.importer.sources.steam_source import SteamSource
|
||||||
from src.utils.create_dialog import create_dialog
|
from src.utils.create_dialog import create_dialog
|
||||||
|
|
||||||
|
|
||||||
@@ -49,37 +48,36 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
exit_after_launch_switch = Gtk.Template.Child()
|
exit_after_launch_switch = Gtk.Template.Child()
|
||||||
cover_launches_game_switch = Gtk.Template.Child()
|
cover_launches_game_switch = Gtk.Template.Child()
|
||||||
high_quality_images_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_expander_row = Gtk.Template.Child()
|
||||||
steam_action_row = Gtk.Template.Child()
|
steam_data_action_row = Gtk.Template.Child()
|
||||||
steam_file_chooser_button = Gtk.Template.Child()
|
steam_data_file_chooser_button = Gtk.Template.Child()
|
||||||
|
|
||||||
lutris_expander_row = Gtk.Template.Child()
|
lutris_expander_row = Gtk.Template.Child()
|
||||||
lutris_action_row = Gtk.Template.Child()
|
lutris_data_action_row = Gtk.Template.Child()
|
||||||
lutris_file_chooser_button = Gtk.Template.Child()
|
lutris_data_file_chooser_button = Gtk.Template.Child()
|
||||||
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()
|
||||||
|
|
||||||
heroic_expander_row = Gtk.Template.Child()
|
heroic_expander_row = Gtk.Template.Child()
|
||||||
heroic_action_row = Gtk.Template.Child()
|
heroic_config_action_row = Gtk.Template.Child()
|
||||||
heroic_file_chooser_button = Gtk.Template.Child()
|
heroic_config_file_chooser_button = Gtk.Template.Child()
|
||||||
heroic_import_epic_switch = Gtk.Template.Child()
|
heroic_import_epic_switch = Gtk.Template.Child()
|
||||||
heroic_import_gog_switch = Gtk.Template.Child()
|
heroic_import_gog_switch = Gtk.Template.Child()
|
||||||
heroic_import_sideload_switch = Gtk.Template.Child()
|
heroic_import_sideload_switch = Gtk.Template.Child()
|
||||||
|
|
||||||
bottles_expander_row = Gtk.Template.Child()
|
bottles_expander_row = Gtk.Template.Child()
|
||||||
bottles_action_row = Gtk.Template.Child()
|
bottles_data_action_row = Gtk.Template.Child()
|
||||||
bottles_file_chooser_button = Gtk.Template.Child()
|
bottles_data_file_chooser_button = Gtk.Template.Child()
|
||||||
|
|
||||||
itch_expander_row = Gtk.Template.Child()
|
itch_expander_row = Gtk.Template.Child()
|
||||||
itch_action_row = Gtk.Template.Child()
|
itch_config_action_row = Gtk.Template.Child()
|
||||||
itch_file_chooser_button = Gtk.Template.Child()
|
itch_config_file_chooser_button = Gtk.Template.Child()
|
||||||
|
|
||||||
legendary_expander_row = Gtk.Template.Child()
|
legendary_expander_row = Gtk.Template.Child()
|
||||||
legendary_action_row = Gtk.Template.Child()
|
legendary_config_action_row = Gtk.Template.Child()
|
||||||
legendary_file_chooser_button = Gtk.Template.Child()
|
legendary_config_file_chooser_button = 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()
|
||||||
@@ -89,6 +87,9 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
sgdb_animated_switch = Gtk.Template.Child()
|
sgdb_animated_switch = Gtk.Template.Child()
|
||||||
|
|
||||||
danger_zone_group = 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()
|
removed_games = set()
|
||||||
|
|
||||||
@@ -116,87 +117,25 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
if shared.PROFILE == "development":
|
if shared.PROFILE == "development":
|
||||||
|
self.reset_action_row.set_visible(True)
|
||||||
def reset_app(*_args):
|
self.reset_button.connect("clicked", self.reset_app)
|
||||||
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.set_default_size(-1, 560)
|
self.set_default_size(-1, 560)
|
||||||
|
|
||||||
# Steam
|
# Sources settings
|
||||||
self.create_preferences(self, "steam", "Steam")
|
for source_class in (
|
||||||
|
BottlesSource,
|
||||||
# Lutris
|
HeroicSource,
|
||||||
self.create_preferences(self, "lutris", "Lutris")
|
ItchSource,
|
||||||
|
LegendarySource,
|
||||||
def set_cache_dir(_source, result, *_args):
|
LutrisSource,
|
||||||
try:
|
SteamSource,
|
||||||
path = Path(self.file_chooser.select_folder_finish(result).get_path())
|
):
|
||||||
except GLib.GError:
|
source = source_class()
|
||||||
return
|
if not source.is_available:
|
||||||
|
expander_row = getattr(self, f"{source.id}_expander_row")
|
||||||
def response(widget, response):
|
expander_row.remove()
|
||||||
if response == "choose_folder":
|
|
||||||
self.choose_folder(widget, set_cache_dir)
|
|
||||||
|
|
||||||
if lutris_cache_exists(path):
|
|
||||||
self.set_subtitle(self, "lutris-cache")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
create_dialog(
|
self.init_source_row(source)
|
||||||
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)
|
|
||||||
|
|
||||||
# SteamGridDB
|
# SteamGridDB
|
||||||
def sgdb_key_changed(*_args):
|
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):
|
def get_switch(self, setting):
|
||||||
return getattr(self, f'{setting.replace("-", "_")}_switch')
|
return getattr(self, f'{setting.replace("-", "_")}_switch')
|
||||||
|
|
||||||
@@ -255,8 +189,8 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
Gio.SettingsBindFlags.DEFAULT,
|
Gio.SettingsBindFlags.DEFAULT,
|
||||||
)
|
)
|
||||||
|
|
||||||
def choose_folder(self, _widget, function):
|
def choose_folder(self, _widget, callback, callback_data=None):
|
||||||
self.file_chooser.select_folder(self.win, None, function, None)
|
self.file_chooser.select_folder(self.win, None, callback, callback_data)
|
||||||
|
|
||||||
def undo_remove_all(self, *_args):
|
def undo_remove_all(self, *_args):
|
||||||
for game in self.removed_games:
|
for game in self.removed_games:
|
||||||
@@ -281,53 +215,98 @@ class PreferencesWindow(Adw.PreferencesWindow):
|
|||||||
|
|
||||||
self.add_toast(self.toast)
|
self.add_toast(self.toast)
|
||||||
|
|
||||||
def set_subtitle(self, win, source_id):
|
def reset_app(*_args):
|
||||||
getattr(win, f'{source_id.replace("-", "_")}_action_row').set_subtitle(
|
rmtree(shared.data_dir / "cartridges", True)
|
||||||
# Remove the path if the dir is picked via the Flatpak portal
|
rmtree(shared.config_dir / "cartridges", True)
|
||||||
re.sub(
|
rmtree(shared.cache_dir / "cartridges", True)
|
||||||
"/run/user/\\d*/doc/.*/",
|
|
||||||
"",
|
for key in (
|
||||||
str(
|
(settings_schema_source := Gio.SettingsSchemaSource.get_default())
|
||||||
Path(shared.schema.get_string(f"{source_id}-location")).expanduser()
|
.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:
|
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:
|
except GLib.GError:
|
||||||
return
|
return
|
||||||
|
|
||||||
def response(widget, response):
|
# Good picked location
|
||||||
if response == "choose_folder":
|
location = getattr(source, f"{location_name}_location")
|
||||||
win.choose_folder(widget, set_dir)
|
if location.check_candidate(path):
|
||||||
|
# Set the schema
|
||||||
if globals()[f"{source_id}_installed"](path):
|
infix = "-cache" if location == "cache" else ""
|
||||||
self.set_subtitle(win, source_id)
|
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:
|
else:
|
||||||
create_dialog(
|
if location_name == "cache":
|
||||||
win,
|
title = "Cache not found"
|
||||||
_("Installation Not Found"),
|
subtitle_format = "Select the {} cache directory."
|
||||||
# The variable is the name of the game launcher
|
else:
|
||||||
_("Select the {} configuration directory.").format(name) if config
|
title = "Installation not found"
|
||||||
# The variable is the name of the game launcher
|
subtitle_format = "Select the {} installation directory."
|
||||||
else _("Select the {} data directory.").format(name),
|
dialog = create_dialog(
|
||||||
|
self,
|
||||||
|
_(title),
|
||||||
|
_(subtitle_format).format(source.name),
|
||||||
"choose_folder",
|
"choose_folder",
|
||||||
_("Set Location"),
|
_("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(
|
shared.schema.bind(
|
||||||
source_id,
|
source.id,
|
||||||
getattr(win, f"{source_id}_expander_row"),
|
expander_row,
|
||||||
"enable-expansion",
|
"enable-expansion",
|
||||||
Gio.SettingsBindFlags.DEFAULT,
|
Gio.SettingsBindFlags.DEFAULT,
|
||||||
)
|
)
|
||||||
|
|
||||||
getattr(win, f"{source_id}_file_chooser_button").connect(
|
# Connect dir picker buttons
|
||||||
"clicked", win.choose_folder, set_dir
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user