mirror of
https://github.com/lutris/lutris
synced 2024-10-14 03:32:52 +00:00
Allow downloading of GOG extras (Closes #2945)
This commit is contained in:
parent
88e4ffa6bb
commit
427ef2f8ec
|
@ -22,6 +22,8 @@ def save_cache_path(path):
|
|||
|
||||
def save_to_cache(source, destination):
|
||||
"""Copy a file or folder to the cache"""
|
||||
if not source:
|
||||
raise ValueError("No source given to save")
|
||||
if os.path.dirname(source) == destination:
|
||||
logger.info("File is already cached in %s, skipping", destination)
|
||||
return
|
||||
|
|
0
lutris/gui/installer/__init__.py
Normal file
0
lutris/gui/installer/__init__.py
Normal file
|
@ -1,133 +1,18 @@
|
|||
"""Widgets for the installer window"""
|
||||
import os
|
||||
from gettext import gettext as _
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from gi.repository import GObject, Gtk, Pango
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
from lutris.cache import save_to_cache
|
||||
from lutris.gui.installer.widgets import InstallerLabel
|
||||
from lutris.gui.widgets.common import FileChooserEntry
|
||||
from lutris.gui.widgets.download_progress import DownloadProgressBox
|
||||
from lutris.installer.steam_installer import SteamInstaller
|
||||
from lutris.util import system
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.strings import add_url_tags, gtk_safe
|
||||
|
||||
|
||||
class InstallerLabel(Gtk.Label):
|
||||
"""A label for installers"""
|
||||
|
||||
def __init__(self, text, wrap=True):
|
||||
super().__init__()
|
||||
if wrap:
|
||||
self.set_line_wrap(True)
|
||||
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
|
||||
else:
|
||||
self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
|
||||
self.set_alignment(0, 0.5)
|
||||
self.set_margin_right(12)
|
||||
self.set_markup(text)
|
||||
self.props.can_focus = False
|
||||
self.set_tooltip_text(text)
|
||||
|
||||
|
||||
class InstallerScriptBox(Gtk.VBox):
|
||||
"""Box displaying the details of a script, with associated action buttons"""
|
||||
|
||||
def __init__(self, script, parent=None, revealed=False):
|
||||
super().__init__()
|
||||
self.script = script
|
||||
self.parent = parent
|
||||
self.revealer = None
|
||||
self.set_margin_left(12)
|
||||
self.set_margin_right(12)
|
||||
box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
|
||||
box.pack_start(self.get_infobox(), True, True, 0)
|
||||
box.add(self.get_install_button())
|
||||
self.add(box)
|
||||
self.add(self.get_revealer(revealed))
|
||||
|
||||
def get_rating(self):
|
||||
"""Return a string representation of the API rating"""
|
||||
try:
|
||||
rating = int(self.script["rating"])
|
||||
except (ValueError, TypeError, KeyError):
|
||||
return ""
|
||||
return "⭐" * rating
|
||||
|
||||
def get_infobox(self):
|
||||
"""Return the central information box"""
|
||||
info_box = Gtk.VBox(spacing=6)
|
||||
title_box = Gtk.HBox(spacing=6)
|
||||
title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
|
||||
title_box.pack_start(InstallerLabel(""), True, True, 0)
|
||||
rating_label = InstallerLabel(self.get_rating())
|
||||
rating_label.set_alignment(1, 0.5)
|
||||
title_box.pack_end(rating_label, False, False, 0)
|
||||
info_box.add(title_box)
|
||||
info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
|
||||
return info_box
|
||||
|
||||
def get_revealer(self, revealed):
|
||||
"""Return the revelaer widget"""
|
||||
self.revealer = Gtk.Revealer()
|
||||
self.revealer.add(self.get_notes())
|
||||
self.revealer.set_reveal_child(revealed)
|
||||
return self.revealer
|
||||
|
||||
def get_install_button(self):
|
||||
"""Return the install button widget"""
|
||||
align = Gtk.Alignment()
|
||||
align.set(0, 0, 0, 0)
|
||||
|
||||
install_button = Gtk.Button(_("Install"))
|
||||
install_button.connect("clicked", self.on_install_clicked)
|
||||
align.add(install_button)
|
||||
return align
|
||||
|
||||
def get_notes(self):
|
||||
"""Return the notes widget"""
|
||||
notes = self.script["notes"].strip()
|
||||
if not notes:
|
||||
return Gtk.Alignment()
|
||||
notes_label = InstallerLabel(notes)
|
||||
notes_label.set_margin_top(12)
|
||||
notes_label.set_margin_bottom(12)
|
||||
notes_label.set_margin_right(12)
|
||||
notes_label.set_margin_left(12)
|
||||
return notes_label
|
||||
|
||||
def reveal(self, reveal=True):
|
||||
"""Show or hide the information in the revealer"""
|
||||
if self.revealer:
|
||||
self.revealer.set_reveal_child(reveal)
|
||||
|
||||
def on_install_clicked(self, _widget):
|
||||
"""Handler to notify the parent of the selected installer"""
|
||||
self.parent.emit("installer-selected", self.script["slug"])
|
||||
|
||||
|
||||
class InstallerPicker(Gtk.ListBox):
|
||||
"""List box to pick between several installers"""
|
||||
|
||||
__gsignals__ = {"installer-selected": (GObject.SIGNAL_RUN_FIRST, None, (str, ))}
|
||||
|
||||
def __init__(self, scripts):
|
||||
super().__init__()
|
||||
revealed = True
|
||||
for script in scripts:
|
||||
self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
|
||||
revealed = False # Only reveal the first installer.
|
||||
self.connect('row-selected', self.on_activate)
|
||||
self.show_all()
|
||||
|
||||
@staticmethod
|
||||
def on_activate(widget, row):
|
||||
"""Handler for hiding and showing the revealers in children"""
|
||||
for script_box_row in widget:
|
||||
script_box = script_box_row.get_children()[0]
|
||||
script_box.reveal(False)
|
||||
installer_row = row.get_children()[0]
|
||||
installer_row.reveal()
|
||||
from lutris.util.strings import gtk_safe
|
||||
|
||||
|
||||
class InstallerFileBox(Gtk.VBox):
|
||||
|
@ -185,6 +70,8 @@ class InstallerFileBox(Gtk.VBox):
|
|||
def get_file_provider_widget(self):
|
||||
"""Return the widget used to track progress of file"""
|
||||
box = Gtk.VBox(spacing=6)
|
||||
print("PROVIDER")
|
||||
print(self.provider)
|
||||
if self.provider == "download":
|
||||
download_progress = self.get_download_progress()
|
||||
box.pack_start(download_progress, False, False, 0)
|
||||
|
@ -218,8 +105,17 @@ class InstallerFileBox(Gtk.VBox):
|
|||
info_box.add(self.state_label)
|
||||
steam_box.add(info_box)
|
||||
return steam_box
|
||||
return Gtk.Label(self.get_file_label())
|
||||
|
||||
return Gtk.Label(gtk_safe(self.installer_file.url))
|
||||
def get_file_label(self):
|
||||
"""Return a human readable label for installer files"""
|
||||
url = self.installer_file.url
|
||||
if url.startswith("http"):
|
||||
parsed = urlparse(url)
|
||||
label = "%s on %s" % (self.installer_file.filename, parsed.netloc)
|
||||
else:
|
||||
label = url
|
||||
return gtk_safe(label)
|
||||
|
||||
def get_popover(self):
|
||||
"""Return the popover widget to select file source"""
|
||||
|
@ -284,14 +180,14 @@ class InstallerFileBox(Gtk.VBox):
|
|||
|
||||
def get_source_button_label(self):
|
||||
"""Return the label for the source button"""
|
||||
if self.provider == "download":
|
||||
return _("Download")
|
||||
if self.provider == "pga":
|
||||
return _("Cache")
|
||||
if self.provider == "user":
|
||||
return _("Local")
|
||||
if self.provider == "steam":
|
||||
return _("Steam")
|
||||
provider_labels = {
|
||||
"download": _("Download"),
|
||||
"pga": _("Cache"),
|
||||
"user": _("Local"),
|
||||
"steam": _("Steam"),
|
||||
}
|
||||
if self.provider in provider_labels:
|
||||
return provider_labels[self.provider]
|
||||
raise ValueError("Unsupported provider %s" % self.provider)
|
||||
|
||||
def get_file_provider_label(self):
|
||||
|
@ -312,7 +208,7 @@ class InstallerFileBox(Gtk.VBox):
|
|||
cache_option.connect("toggled", self.on_user_file_cached)
|
||||
box.pack_start(cache_option, False, False, 0)
|
||||
return box
|
||||
return InstallerLabel(gtk_safe(self.installer_file.human_url), wrap=False)
|
||||
return InstallerLabel(self.get_file_label(), wrap=False)
|
||||
|
||||
def get_widgets(self):
|
||||
"""Return the widget with the source of the file and a way to change its source"""
|
||||
|
@ -383,77 +279,3 @@ class InstallerFileBox(Gtk.VBox):
|
|||
self.installer_file.dest_file = widget.get_steam_data_path()
|
||||
self.emit("file-available")
|
||||
self.cache_file()
|
||||
|
||||
|
||||
class InstallerFilesBox(Gtk.ListBox):
|
||||
"""List box presenting all files needed for an installer"""
|
||||
|
||||
__gsignals__ = {
|
||||
"files-ready": (GObject.SIGNAL_RUN_LAST, None, (bool, )),
|
||||
"files-available": (GObject.SIGNAL_RUN_LAST, None, ())
|
||||
}
|
||||
|
||||
def __init__(self, installer_files, parent):
|
||||
super().__init__()
|
||||
self.parent = parent
|
||||
self.installer_files = installer_files
|
||||
self.ready_files = set()
|
||||
self.available_files = set()
|
||||
self.installer_files_boxes = {}
|
||||
for installer_file in installer_files:
|
||||
installer_file_box = InstallerFileBox(installer_file)
|
||||
installer_file_box.connect("file-ready", self.on_file_ready)
|
||||
installer_file_box.connect("file-unready", self.on_file_unready)
|
||||
installer_file_box.connect("file-available", self.on_file_available)
|
||||
self.installer_files_boxes[installer_file.id] = installer_file_box
|
||||
self.add(installer_file_box)
|
||||
if installer_file_box.is_ready:
|
||||
self.ready_files.add(installer_file.id)
|
||||
self.show_all()
|
||||
self.check_files_ready()
|
||||
|
||||
def start_all(self):
|
||||
"""Start all downloads"""
|
||||
for file_id in self.installer_files_boxes:
|
||||
self.installer_files_boxes[file_id].start()
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
"""Return True if all files are ready to be fetched"""
|
||||
return len(self.ready_files) == len(self.installer_files)
|
||||
|
||||
def check_files_ready(self):
|
||||
"""Checks if all installer files are ready and emit a signal if so"""
|
||||
logger.debug("Files are ready? %s", self.is_ready)
|
||||
self.emit("files-ready", self.is_ready)
|
||||
|
||||
def on_file_ready(self, widget):
|
||||
"""Fired when a file has a valid provider.
|
||||
If the file is user provided, it must set to a valid path.
|
||||
"""
|
||||
file_id = widget.installer_file.id
|
||||
self.ready_files.add(file_id)
|
||||
self.check_files_ready()
|
||||
|
||||
def on_file_unready(self, widget):
|
||||
"""Fired when a file can't be provided.
|
||||
Blocks the installer from continuing.
|
||||
"""
|
||||
file_id = widget.installer_file.id
|
||||
self.ready_files.remove(file_id)
|
||||
self.check_files_ready()
|
||||
|
||||
def on_file_available(self, widget):
|
||||
"""A new file is available"""
|
||||
file_id = widget.installer_file.id
|
||||
self.available_files.add(file_id)
|
||||
if len(self.available_files) == len(self.installer_files):
|
||||
logger.info("All files available")
|
||||
self.emit("files-available")
|
||||
|
||||
def get_game_files(self):
|
||||
"""Return a mapping of the local files usable by the interpreter"""
|
||||
return {
|
||||
installer_file.id: installer_file.dest_file
|
||||
for installer_file in self.installer_files
|
||||
}
|
78
lutris/gui/installer/files_box.py
Normal file
78
lutris/gui/installer/files_box.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from gi.repository import GObject, Gtk
|
||||
|
||||
from lutris.gui.installer.file_box import InstallerFileBox
|
||||
from lutris.util.log import logger
|
||||
|
||||
|
||||
class InstallerFilesBox(Gtk.ListBox):
|
||||
"""List box presenting all files needed for an installer"""
|
||||
|
||||
__gsignals__ = {
|
||||
"files-ready": (GObject.SIGNAL_RUN_LAST, None, (bool, )),
|
||||
"files-available": (GObject.SIGNAL_RUN_LAST, None, ())
|
||||
}
|
||||
|
||||
def __init__(self, installer_files, parent):
|
||||
super().__init__()
|
||||
self.parent = parent
|
||||
self.installer_files = installer_files
|
||||
self.ready_files = set()
|
||||
self.available_files = set()
|
||||
self.installer_files_boxes = {}
|
||||
for installer_file in installer_files:
|
||||
installer_file_box = InstallerFileBox(installer_file)
|
||||
installer_file_box.connect("file-ready", self.on_file_ready)
|
||||
installer_file_box.connect("file-unready", self.on_file_unready)
|
||||
installer_file_box.connect("file-available", self.on_file_available)
|
||||
self.installer_files_boxes[installer_file.id] = installer_file_box
|
||||
self.add(installer_file_box)
|
||||
if installer_file_box.is_ready:
|
||||
self.ready_files.add(installer_file.id)
|
||||
self.show_all()
|
||||
self.check_files_ready()
|
||||
|
||||
def start_all(self):
|
||||
"""Start all downloads"""
|
||||
for file_id in self.installer_files_boxes:
|
||||
self.installer_files_boxes[file_id].start()
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
"""Return True if all files are ready to be fetched"""
|
||||
return len(self.ready_files) == len(self.installer_files)
|
||||
|
||||
def check_files_ready(self):
|
||||
"""Checks if all installer files are ready and emit a signal if so"""
|
||||
logger.debug("Files are ready? %s", self.is_ready)
|
||||
self.emit("files-ready", self.is_ready)
|
||||
|
||||
def on_file_ready(self, widget):
|
||||
"""Fired when a file has a valid provider.
|
||||
If the file is user provided, it must set to a valid path.
|
||||
"""
|
||||
file_id = widget.installer_file.id
|
||||
self.ready_files.add(file_id)
|
||||
self.check_files_ready()
|
||||
|
||||
def on_file_unready(self, widget):
|
||||
"""Fired when a file can't be provided.
|
||||
Blocks the installer from continuing.
|
||||
"""
|
||||
file_id = widget.installer_file.id
|
||||
self.ready_files.remove(file_id)
|
||||
self.check_files_ready()
|
||||
|
||||
def on_file_available(self, widget):
|
||||
"""A new file is available"""
|
||||
file_id = widget.installer_file.id
|
||||
self.available_files.add(file_id)
|
||||
if len(self.available_files) == len(self.installer_files):
|
||||
logger.info("All files available")
|
||||
self.emit("files-available")
|
||||
|
||||
def get_game_files(self):
|
||||
"""Return a mapping of the local files usable by the interpreter"""
|
||||
return {
|
||||
installer_file.id: installer_file.dest_file
|
||||
for installer_file in self.installer_files
|
||||
}
|
27
lutris/gui/installer/picker.py
Normal file
27
lutris/gui/installer/picker.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from gi.repository import GObject, Gtk
|
||||
|
||||
from lutris.gui.installer.script_box import InstallerScriptBox
|
||||
|
||||
|
||||
class InstallerPicker(Gtk.ListBox):
|
||||
"""List box to pick between several installers"""
|
||||
|
||||
__gsignals__ = {"installer-selected": (GObject.SIGNAL_RUN_FIRST, None, (str, ))}
|
||||
|
||||
def __init__(self, scripts):
|
||||
super().__init__()
|
||||
revealed = True
|
||||
for script in scripts:
|
||||
self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
|
||||
revealed = False # Only reveal the first installer.
|
||||
self.connect('row-selected', self.on_activate)
|
||||
self.show_all()
|
||||
|
||||
@staticmethod
|
||||
def on_activate(widget, row):
|
||||
"""Handler for hiding and showing the revealers in children"""
|
||||
for script_box_row in widget:
|
||||
script_box = script_box_row.get_children()[0]
|
||||
script_box.reveal(False)
|
||||
installer_row = row.get_children()[0]
|
||||
installer_row.reveal()
|
82
lutris/gui/installer/script_box.py
Normal file
82
lutris/gui/installer/script_box.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
from gettext import gettext as _
|
||||
|
||||
from gi.repository import Gtk
|
||||
|
||||
from lutris.gui.installer.widgets import InstallerLabel
|
||||
from lutris.util.strings import add_url_tags, gtk_safe
|
||||
|
||||
|
||||
class InstallerScriptBox(Gtk.VBox):
|
||||
"""Box displaying the details of a script, with associated action buttons"""
|
||||
|
||||
def __init__(self, script, parent=None, revealed=False):
|
||||
super().__init__()
|
||||
self.script = script
|
||||
self.parent = parent
|
||||
self.revealer = None
|
||||
self.set_margin_left(12)
|
||||
self.set_margin_right(12)
|
||||
box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
|
||||
box.pack_start(self.get_infobox(), True, True, 0)
|
||||
box.add(self.get_install_button())
|
||||
self.add(box)
|
||||
self.add(self.get_revealer(revealed))
|
||||
|
||||
def get_rating(self):
|
||||
"""Return a string representation of the API rating"""
|
||||
try:
|
||||
rating = int(self.script["rating"])
|
||||
except (ValueError, TypeError, KeyError):
|
||||
return ""
|
||||
return "⭐" * rating
|
||||
|
||||
def get_infobox(self):
|
||||
"""Return the central information box"""
|
||||
info_box = Gtk.VBox(spacing=6)
|
||||
title_box = Gtk.HBox(spacing=6)
|
||||
title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
|
||||
title_box.pack_start(InstallerLabel(""), True, True, 0)
|
||||
rating_label = InstallerLabel(self.get_rating())
|
||||
rating_label.set_alignment(1, 0.5)
|
||||
title_box.pack_end(rating_label, False, False, 0)
|
||||
info_box.add(title_box)
|
||||
info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
|
||||
return info_box
|
||||
|
||||
def get_revealer(self, revealed):
|
||||
"""Return the revelaer widget"""
|
||||
self.revealer = Gtk.Revealer()
|
||||
self.revealer.add(self.get_notes())
|
||||
self.revealer.set_reveal_child(revealed)
|
||||
return self.revealer
|
||||
|
||||
def get_install_button(self):
|
||||
"""Return the install button widget"""
|
||||
align = Gtk.Alignment()
|
||||
align.set(0, 0, 0, 0)
|
||||
|
||||
install_button = Gtk.Button(_("Install"))
|
||||
install_button.connect("clicked", self.on_install_clicked)
|
||||
align.add(install_button)
|
||||
return align
|
||||
|
||||
def get_notes(self):
|
||||
"""Return the notes widget"""
|
||||
notes = self.script["notes"].strip()
|
||||
if not notes:
|
||||
return Gtk.Alignment()
|
||||
notes_label = InstallerLabel(notes)
|
||||
notes_label.set_margin_top(12)
|
||||
notes_label.set_margin_bottom(12)
|
||||
notes_label.set_margin_right(12)
|
||||
notes_label.set_margin_left(12)
|
||||
return notes_label
|
||||
|
||||
def reveal(self, reveal=True):
|
||||
"""Show or hide the information in the revealer"""
|
||||
if self.revealer:
|
||||
self.revealer.set_reveal_child(reveal)
|
||||
|
||||
def on_install_clicked(self, _widget):
|
||||
"""Handler to notify the parent of the selected installer"""
|
||||
self.parent.emit("installer-selected", self.script["slug"])
|
18
lutris/gui/installer/widgets.py
Normal file
18
lutris/gui/installer/widgets.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from gi.repository import Gtk, Pango
|
||||
|
||||
|
||||
class InstallerLabel(Gtk.Label):
|
||||
"""A label for installers"""
|
||||
|
||||
def __init__(self, text, wrap=True):
|
||||
super().__init__()
|
||||
if wrap:
|
||||
self.set_line_wrap(True)
|
||||
self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
|
||||
else:
|
||||
self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
|
||||
self.set_alignment(0, 0.5)
|
||||
self.set_margin_right(12)
|
||||
self.set_markup(text)
|
||||
self.props.can_focus = False
|
||||
self.set_tooltip_text(text)
|
|
@ -10,15 +10,16 @@ from lutris.exceptions import UnavailableGame
|
|||
from lutris.game import Game
|
||||
from lutris.gui.dialogs import DirectoryDialog, InstallerSourceDialog, QuestionDialog
|
||||
from lutris.gui.dialogs.cache import CacheConfigurationDialog
|
||||
from lutris.gui.installer.files_box import InstallerFilesBox
|
||||
from lutris.gui.installer.picker import InstallerPicker
|
||||
from lutris.gui.widgets.common import FileChooserEntry, InstallerLabel
|
||||
from lutris.gui.widgets.installer import InstallerFilesBox, InstallerPicker
|
||||
from lutris.gui.widgets.log_text_view import LogTextView
|
||||
from lutris.gui.widgets.window import BaseApplicationWindow
|
||||
from lutris.installer import interpreter
|
||||
from lutris.installer.errors import MissingGameDependency, ScriptingError
|
||||
from lutris.util import xdgshortcuts
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.strings import add_url_tags, gtk_safe
|
||||
from lutris.util.strings import add_url_tags, gtk_safe, human_size
|
||||
|
||||
|
||||
class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public-methods
|
||||
|
@ -288,9 +289,14 @@ class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public
|
|||
"""Enable continue button if a non-empty choice is selected"""
|
||||
self.continue_button.set_sensitive(bool(widget.get_active_id()))
|
||||
|
||||
def on_runners_ready(self, _widget):
|
||||
def on_runners_ready(self, _widget=None):
|
||||
"""The runners are ready, proceed with file selection"""
|
||||
self.clean_widgets()
|
||||
if self.interpreter.extras is None:
|
||||
extras = self.interpreter.get_extras()
|
||||
if extras:
|
||||
self.show_extras(extras)
|
||||
return
|
||||
try:
|
||||
self.interpreter.installer.prepare_game_files()
|
||||
except UnavailableGame as ex:
|
||||
|
@ -320,6 +326,67 @@ class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public
|
|||
"clicked", self.on_files_confirmed, installer_files_box
|
||||
)
|
||||
|
||||
def get_extra_label(self, extra):
|
||||
label = extra["name"]
|
||||
_infos = []
|
||||
if extra.get("total_size"):
|
||||
_infos.append(human_size(extra["total_size"]))
|
||||
if extra.get("type"):
|
||||
_infos.append(extra["type"])
|
||||
if _infos:
|
||||
label += " (%s)" % ", ".join(_infos)
|
||||
return label
|
||||
|
||||
def show_extras(self, extras):
|
||||
extra_liststore = Gtk.ListStore(
|
||||
bool, # is selected?
|
||||
str, # id
|
||||
str, # label
|
||||
)
|
||||
for extra in extras:
|
||||
extra_liststore.append((False, extra["id"], self.get_extra_label(extra)))
|
||||
|
||||
treeview = Gtk.TreeView(extra_liststore)
|
||||
treeview.set_headers_visible(False)
|
||||
renderer_toggle = Gtk.CellRendererToggle()
|
||||
renderer_toggle.connect("toggled", self.on_extra_toggled, extra_liststore)
|
||||
renderer_text = Gtk.CellRendererText()
|
||||
|
||||
installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=0)
|
||||
treeview.append_column(installed_column)
|
||||
|
||||
label_column = Gtk.TreeViewColumn(None, renderer_text)
|
||||
label_column.add_attribute(renderer_text, "text", 2)
|
||||
label_column.set_property("min-width", 80)
|
||||
treeview.append_column(label_column)
|
||||
|
||||
scrolledwindow = Gtk.ScrolledWindow(
|
||||
hexpand=True,
|
||||
vexpand=True,
|
||||
child=treeview,
|
||||
visible=True
|
||||
)
|
||||
scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
scrolledwindow.show_all()
|
||||
self.widget_box.pack_end(scrolledwindow, True, True, 10)
|
||||
self.continue_button.show()
|
||||
self.continue_button.set_sensitive(True)
|
||||
self.continue_handler = self.continue_button.connect("clicked", self.on_extras_confirmed, extra_liststore)
|
||||
|
||||
def on_extra_toggled(self, _widget, path, store):
|
||||
row = store[path]
|
||||
row[0] = not row[0]
|
||||
|
||||
def on_extras_confirmed(self, _button, extra_store):
|
||||
logger.debug("Extras confirmed")
|
||||
selected_extras = []
|
||||
for extra in extra_store:
|
||||
if extra[0]:
|
||||
selected_extras.append(extra[1])
|
||||
self.interpreter.extras = selected_extras
|
||||
print(selected_extras)
|
||||
self.on_runners_ready()
|
||||
|
||||
def on_files_ready(self, _widget, is_ready):
|
||||
"""Toggle state of continue button based on ready state"""
|
||||
logger.debug("Files are ready: %s", is_ready)
|
||||
|
@ -458,7 +525,7 @@ class InstallerWindow(BaseApplicationWindow): # pylint: disable=too-many-public
|
|||
self.widget_box.pack_start(label, False, False, 18)
|
||||
|
||||
def add_spinner(self):
|
||||
"""Display a wait icon."""
|
||||
"""Show a spinner in the middle of the view"""
|
||||
self.clean_widgets()
|
||||
spinner = Gtk.Spinner()
|
||||
self.widget_box.pack_start(spinner, False, False, 18)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
# Standard Library
|
||||
from gettext import gettext as _
|
||||
|
||||
# Third Party Libraries
|
||||
from gi.repository import GLib, GObject, Gtk, Pango
|
||||
|
||||
# Lutris Modules
|
||||
from lutris.util.downloader import Downloader
|
||||
from lutris.util.strings import gtk_safe
|
||||
|
||||
|
||||
class DownloadProgressBox(Gtk.Box):
|
||||
|
@ -93,7 +91,7 @@ class DownloadProgressBox(Gtk.Box):
|
|||
if self.downloader.state == self.downloader.CANCELLED:
|
||||
self._set_text(_("Download interrupted"))
|
||||
else:
|
||||
self._set_text(self.downloader.error)
|
||||
self._set_text(self.downloader.error[:80])
|
||||
if self.downloader.state == self.downloader.CANCELLED:
|
||||
self.emit("cancel", {})
|
||||
return False
|
||||
|
@ -115,5 +113,5 @@ class DownloadProgressBox(Gtk.Box):
|
|||
return True
|
||||
|
||||
def _set_text(self, text):
|
||||
markup = u"<span size='10000'>{}</span>".format(text)
|
||||
markup = u"<span size='10000'>{}</span>".format(gtk_safe(text))
|
||||
self.progress_label.set_markup(markup)
|
||||
|
|
|
@ -144,9 +144,15 @@ class LutrisInstaller: # pylint: disable=too-many-instance-attributes
|
|||
if not installer_file_id:
|
||||
logger.warning("Could not find a file for this service")
|
||||
return
|
||||
logger.info("Should install %s", self.interpreter.extras)
|
||||
if self.service.has_extras:
|
||||
self.service.selected_extras = self.interpreter.extras
|
||||
installer_files = self.service.get_installer_files(self, installer_file_id)
|
||||
for installer_file in installer_files:
|
||||
self.files.append(installer_file)
|
||||
if not installer_files:
|
||||
# Failed to get the service game, put back a user provided file
|
||||
self.files.insert(0, "N/A: Provider installer file")
|
||||
|
||||
def _substitute_config(self, script_config):
|
||||
"""Substitute values such as $GAMEDIR in a config dict."""
|
||||
|
|
|
@ -36,6 +36,9 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
|
|||
self.service = parent.service if parent else None
|
||||
self.appid = parent.appid if parent else None
|
||||
self.game_dir_created = False # Whether a game folder was created during the install
|
||||
# Extra files for installers, either None if the extras haven't been checked yet.
|
||||
# Or a list of IDs of extras to be downloaded during the install
|
||||
self.extras = None
|
||||
self.game_disc = None
|
||||
self.game_files = {}
|
||||
self.cancelled = False
|
||||
|
@ -147,6 +150,13 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
|
|||
self.target_path = game["directory"]
|
||||
self.requires = game["installer_slug"]
|
||||
|
||||
def get_extras(self):
|
||||
"""Get extras and store them to move them at the end of the install"""
|
||||
if not self.service or not self.service.has_extras:
|
||||
self.extras = []
|
||||
self.extras = self.service.get_extras(self.appid)
|
||||
return self.extras
|
||||
|
||||
def launch_install(self):
|
||||
"""Launch the install process"""
|
||||
self.runners_to_install = self.get_runners_to_install()
|
||||
|
@ -262,6 +272,12 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
|
|||
os.chdir(self.target_path)
|
||||
if not os.path.exists(self.cache_path):
|
||||
os.mkdir(self.cache_path)
|
||||
|
||||
# Copy extras to game folder
|
||||
for extra in self.extras:
|
||||
self.installer.script["installer"].append(
|
||||
{"copy": {"src": extra, "dst": "$GAMEDIR/extras"}}
|
||||
)
|
||||
self._iter_commands()
|
||||
|
||||
def _iter_commands(self, result=None, exception=None):
|
||||
|
|
|
@ -20,6 +20,7 @@ class BaseService(GObject.Object):
|
|||
"""Base class for local services"""
|
||||
id = NotImplemented
|
||||
_matcher = None
|
||||
has_extras = False
|
||||
name = NotImplemented
|
||||
icon = NotImplemented
|
||||
online = False
|
||||
|
|
|
@ -6,7 +6,7 @@ from gettext import gettext as _
|
|||
from urllib.parse import parse_qsl, urlencode, urlparse
|
||||
|
||||
from lutris import settings
|
||||
from lutris.exceptions import AuthenticationError, MultipleInstallerError, UnavailableGame
|
||||
from lutris.exceptions import AuthenticationError, UnavailableGame
|
||||
from lutris.gui.dialogs import WebConnectDialog
|
||||
from lutris.installer import AUTO_ELF_EXE, AUTO_WIN32_EXE
|
||||
from lutris.installer.installer_file import InstallerFile
|
||||
|
@ -64,6 +64,7 @@ class GOGService(OnlineService):
|
|||
id = "gog"
|
||||
name = _("GOG")
|
||||
icon = "gog"
|
||||
has_extras = True
|
||||
medias = {
|
||||
"banner_small": GogSmallBanner,
|
||||
"banner": GogMediumBanner,
|
||||
|
@ -85,6 +86,10 @@ class GOGService(OnlineService):
|
|||
|
||||
is_loading = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.selected_extras = None
|
||||
|
||||
@property
|
||||
def login_url(self):
|
||||
"""Return authentication URL"""
|
||||
|
@ -221,7 +226,6 @@ class GOGService(OnlineService):
|
|||
|
||||
def get_library(self):
|
||||
"""Return the user's library of GOG games"""
|
||||
|
||||
if system.path_exists(self.cache_path):
|
||||
logger.debug("Returning cached GOG library")
|
||||
with open(self.cache_path, "r") as gog_cache:
|
||||
|
@ -265,10 +269,11 @@ class GOGService(OnlineService):
|
|||
logger.info("Getting download info for %s", downlink)
|
||||
try:
|
||||
response = self.make_api_request(downlink)
|
||||
except HTTPError:
|
||||
raise UnavailableGame()
|
||||
except HTTPError as ex:
|
||||
logger.error("HTTP error: %s", ex)
|
||||
raise UnavailableGame
|
||||
if not response:
|
||||
raise UnavailableGame()
|
||||
raise UnavailableGame
|
||||
for field in ("checksum", "downlink"):
|
||||
field_url = response[field]
|
||||
parsed = urlparse(field_url)
|
||||
|
@ -277,15 +282,27 @@ class GOGService(OnlineService):
|
|||
return response
|
||||
|
||||
def get_downloads(self, gogid):
|
||||
"""Return all available downloads for a GOG ID"""
|
||||
gog_data = self.get_game_details(gogid)
|
||||
if not gog_data:
|
||||
logger.warning("Unable to get GOG data for game %s", gogid)
|
||||
return []
|
||||
return gog_data["downloads"]
|
||||
|
||||
def get_installers(self, gogid, runner, language="en"):
|
||||
"""Return available installers for a GOG game"""
|
||||
def get_extras(self, gogid):
|
||||
"""Return a list of bonus content available for a GOG ID"""
|
||||
downloads = self.get_downloads(gogid)
|
||||
return [
|
||||
{
|
||||
"name": download.get("name", ""),
|
||||
"type": download.get("type", ""),
|
||||
"total_size": download.get("total_size", 0),
|
||||
"id": str(download["id"]),
|
||||
} for download in downloads.get("bonus_content") or []
|
||||
]
|
||||
|
||||
def get_installers(self, downloads, runner, language="en"):
|
||||
"""Return available installers for a GOG game"""
|
||||
|
||||
# Filter out Mac installers
|
||||
gog_installers = [installer for installer in downloads["installers"] if installer["os"] != "mac"]
|
||||
|
@ -303,43 +320,66 @@ class GOGService(OnlineService):
|
|||
gog_installers = [installer for installer in gog_installers if installer["language"] == language]
|
||||
return gog_installers
|
||||
|
||||
def get_installer(self, gogid, runner):
|
||||
"""Return a single installer for a given runner"""
|
||||
if not self.is_connected():
|
||||
logger.info("You are not connected to GOG")
|
||||
self.login()
|
||||
if not self.is_connected():
|
||||
raise UnavailableGame
|
||||
gog_installers = self.get_installers(gogid, runner)
|
||||
if len(gog_installers) > 1:
|
||||
raise MultipleInstallerError()
|
||||
try:
|
||||
installer = gog_installers[0]
|
||||
except IndexError:
|
||||
raise UnavailableGame
|
||||
return installer
|
||||
|
||||
def get_gog_download_links(self, gogid, runner):
|
||||
"""Return a list of downloadable links for a GOG game"""
|
||||
installer = self.get_installer(gogid, runner)
|
||||
def query_download_links(self, download):
|
||||
"""Convert files from the GOG API to a format compatible with lutris installers"""
|
||||
download_links = []
|
||||
for game_file in installer.get('files', []):
|
||||
for game_file in download.get("files", []):
|
||||
downlink = game_file.get("downlink")
|
||||
if not downlink:
|
||||
logger.error("No download information for %s", installer)
|
||||
logger.error("No download information for %s", game_file)
|
||||
continue
|
||||
download_info = self.get_download_info(downlink)
|
||||
for field in ('checksum', 'downlink'):
|
||||
download_links.append({"url": download_info[field], "filename": download_info[field + "_filename"]})
|
||||
download_links.append({
|
||||
"name": download.get("name", ""),
|
||||
"os": download.get("os", ""),
|
||||
"type": download.get("type", ""),
|
||||
"total_size": download.get("total_size", 0),
|
||||
"id": str(game_file["id"]),
|
||||
"url": download_info[field],
|
||||
"filename": download_info[field + "_filename"]
|
||||
})
|
||||
return download_links
|
||||
|
||||
def get_extra_files(self, downloads, installer):
|
||||
extra_files = []
|
||||
for extra in downloads["bonus_content"]:
|
||||
if str(extra["id"]) not in self.selected_extras:
|
||||
continue
|
||||
links = self.query_download_links(extra)
|
||||
for link in links:
|
||||
if link["filename"].endswith(".xml"):
|
||||
# GOG gives a link for checksum XML files for bonus content
|
||||
# but downloading them results in a 404 error.
|
||||
continue
|
||||
extra_files.append(
|
||||
InstallerFile(installer.game_slug, str(extra["id"]), {
|
||||
"url": link["url"],
|
||||
"filename": link["filename"],
|
||||
})
|
||||
)
|
||||
return extra_files
|
||||
|
||||
def get_installer_files(self, installer, installer_file_id):
|
||||
if not self.is_connected():
|
||||
self.login()
|
||||
if not self.is_connected():
|
||||
logger.warning("Not connected to GOG, not returning any files")
|
||||
return []
|
||||
try:
|
||||
links = self.get_gog_download_links(installer.service_appid, installer.runner)
|
||||
downloads = self.get_downloads(installer.service_appid)
|
||||
gog_installers = self.get_installers(downloads, installer.runner)
|
||||
if not gog_installers:
|
||||
return []
|
||||
if len(gog_installers) > 1:
|
||||
logger.warning("More than 1 GOG installer found, picking first.")
|
||||
_installer = gog_installers[0]
|
||||
links = self.query_download_links(_installer)
|
||||
except HTTPError:
|
||||
raise UnavailableGame("Couldn't load the download links for this game")
|
||||
if not links:
|
||||
raise UnavailableGame("Could not fing GOG game")
|
||||
|
||||
files = []
|
||||
file_id_provided = False # Only assign installer_file_id once
|
||||
for index, link in enumerate(links):
|
||||
|
@ -361,6 +401,9 @@ class GOGService(OnlineService):
|
|||
)
|
||||
if not file_id_provided:
|
||||
raise UnavailableGame("Unable to determine correct file to launch installer")
|
||||
if self.selected_extras:
|
||||
for extra_file in self.get_extra_files(downloads, installer):
|
||||
files.append(extra_file)
|
||||
return files
|
||||
|
||||
def generate_installer(self, db_game):
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
# Standard Library
|
||||
import os
|
||||
import time
|
||||
|
||||
# Third Party Libraries
|
||||
import requests
|
||||
|
||||
# Lutris Modules
|
||||
from lutris import __version__
|
||||
from lutris.util import jobs
|
||||
from lutris.util.log import logger
|
||||
|
|
1
setup.py
1
setup.py
|
@ -32,6 +32,7 @@ setup(
|
|||
'lutris.gui',
|
||||
'lutris.gui.config',
|
||||
'lutris.gui.dialogs',
|
||||
'lutris.gui.installer',
|
||||
'lutris.gui.views',
|
||||
'lutris.gui.widgets',
|
||||
'lutris.installer',
|
||||
|
|
|
@ -102,7 +102,7 @@ class TestGameDialog(TestCase):
|
|||
self.assertTrue(pga_game)
|
||||
game = Game(pga_game['id'])
|
||||
self.assertEqual(game.name, 'Test game')
|
||||
game.remove(from_library=True)
|
||||
game.remove()
|
||||
|
||||
|
||||
class TestSort(TestCase):
|
||||
|
|
Loading…
Reference in a new issue