Files
faugus-launcher/faugus_proton_manager.py

404 lines
14 KiB
Python

#!/usr/bin/env python3
import requests
import gi
import tarfile
import shutil
import gettext
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib
from faugus.language_config import *
from faugus.dark_theme import *
IS_FLATPAK = 'FLATPAK_ID' in os.environ or os.path.exists('/.flatpak-info')
if IS_FLATPAK:
faugus_png = PathManager.get_icon('io.github.Faugus.faugus-launcher.png')
STEAM_COMPATIBILITY_PATH = Path(os.path.expanduser("~/.local/share/Steam/compatibilitytools.d"))
else:
faugus_png = PathManager.get_icon('faugus-launcher.png')
STEAM_COMPATIBILITY_PATH = Path(PathManager.user_data("Steam/compatibilitytools.d"))
config_file_dir = PathManager.user_config('faugus-launcher/config.ini')
faugus_launcher_dir = PathManager.user_config('faugus-launcher')
try:
translation = gettext.translation('faugus-proton-manager', localedir=LOCALE_DIR, languages=[lang])
translation.install()
_ = translation.gettext
except FileNotFoundError:
gettext.install('faugus-proton-manager', localedir=LOCALE_DIR)
_ = gettext.gettext
class ConfigManager:
def __init__(self):
self.default_config = {
'language': lang,
}
self.config = {}
self.load_config()
def load_config(self):
if os.path.isfile(config_file_dir):
with open(config_file_dir, 'r') as f:
for line in f.read().splitlines():
if '=' in line:
key, value = line.split('=', 1)
self.config[key.strip()] = value.strip().strip('"')
updated = False
for key, default_value in self.default_config.items():
if key not in self.config:
self.config[key] = default_value
updated = True
if updated or not os.path.isfile(config_file_dir):
self.save_config()
def save_config(self):
if not os.path.exists(faugus_launcher_dir):
os.makedirs(faugus_launcher_dir)
with open(config_file_dir, 'w') as f:
for key, value in self.config.items():
f.write(f'{key}={value}\n')
class ProtonDownloader(Gtk.Dialog):
def __init__(self):
super().__init__(title=_("Faugus Proton Manager"))
self.set_resizable(False)
self.set_modal(True)
self.set_icon_from_file(faugus_png)
frame = Gtk.Frame()
frame.set_margin_start(10)
frame.set_margin_end(10)
frame.set_margin_top(10)
frame.set_margin_bottom(10)
self.content_area = self.get_content_area()
self.content_area.set_border_width(0)
self.content_area.set_halign(Gtk.Align.CENTER)
self.content_area.set_valign(Gtk.Align.CENTER)
self.content_area.set_vexpand(True)
self.content_area.set_hexpand(True)
self.content_area.add(frame)
self.progress_label = Gtk.Label(label="")
self.progress_label.set_margin_start(10)
self.progress_label.set_margin_end(10)
self.progress_label.set_margin_bottom(10)
self.content_area.add(self.progress_label)
self.progress_bar = Gtk.ProgressBar()
self.progress_bar.set_margin_start(10)
self.progress_bar.set_margin_end(10)
self.progress_bar.set_margin_bottom(10)
self.content_area.add(self.progress_bar)
self.notebook = Gtk.Notebook()
self.notebook.set_halign(Gtk.Align.FILL)
self.notebook.set_valign(Gtk.Align.FILL)
self.notebook.set_vexpand(True)
self.notebook.set_hexpand(True)
frame.add(self.notebook)
# Tab 1: GE-Proton
self.grid_ge = Gtk.Grid()
self.grid_ge.set_hexpand(True)
self.grid_ge.set_row_spacing(5)
self.grid_ge.set_column_spacing(10)
scroll_ge = Gtk.ScrolledWindow()
scroll_ge.set_size_request(400, 400)
scroll_ge.set_margin_top(10)
scroll_ge.set_margin_bottom(10)
scroll_ge.set_margin_start(10)
scroll_ge.set_margin_end(10)
scroll_ge.add(self.grid_ge)
tab_box_ge = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label_ge = Gtk.Label(label="GE-Proton")
tab_label_ge.set_width_chars(15)
tab_label_ge.set_xalign(0.5)
tab_box_ge.pack_start(tab_label_ge, True, True, 0)
tab_box_ge.set_hexpand(True)
tab_box_ge.show_all()
self.notebook.append_page(scroll_ge, tab_box_ge)
# Tab 2: Proton-EM
self.grid_em = Gtk.Grid()
self.grid_em.set_hexpand(True)
self.grid_em.set_row_spacing(5)
self.grid_em.set_column_spacing(10)
scroll_em = Gtk.ScrolledWindow()
scroll_em.set_size_request(400, 400)
scroll_em.set_margin_top(10)
scroll_em.set_margin_bottom(10)
scroll_em.set_margin_start(10)
scroll_em.set_margin_end(10)
scroll_em.add(self.grid_em)
tab_box_em = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
tab_label_em = Gtk.Label(label="Proton-EM")
tab_label_em.set_width_chars(15)
tab_label_em.set_xalign(0.5)
tab_box_em.pack_start(tab_label_em, True, True, 0)
tab_box_em.set_hexpand(True)
tab_box_em.show_all()
self.notebook.append_page(scroll_em, tab_box_em)
self.load_config()
self.get_releases()
self.show_all()
self.progress_bar.set_visible(False)
self.progress_label.set_visible(False)
def load_config(self):
cfg = ConfigManager()
self.language = cfg.config.get('language', '')
def get_releases(self):
self.fetch_releases_from_url(
"https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases",
self.grid_ge
)
self.fetch_releases_from_url(
"https://api.github.com/repos/Etaash-mathamsetty/Proton/releases",
self.grid_em
)
def fetch_releases_from_url(self, url, grid):
page = 1
releases = []
seen_tags = set()
while True:
response = requests.get(url, params={"page": page, "per_page": 100})
if response.status_code == 200:
page_releases = response.json()
if not page_releases:
break
releases.extend(page_releases)
page += 1
else:
break
for release in releases:
tag_name = release["tag_name"]
if tag_name in seen_tags:
continue
seen_tags.add(tag_name)
if "GloriousEggroll" in url:
if not tag_name.startswith("GE-Proton"):
continue
try:
version_str = tag_name.replace("GE-Proton", "")
major, minor = map(int, version_str.split("-"))
if (major, minor) < (9, 1):
continue
except Exception:
continue
elif "Etaash-mathamsetty" in url:
if not tag_name.startswith("EM-"):
continue
assets = release.get("assets", [])
has_valid_asset = any(
asset["name"].endswith((".tar.gz", ".tar.xz"))
for asset in assets
)
if not has_valid_asset:
continue
self.add_release_to_grid(release, grid)
def add_release_to_grid(self, release, grid):
tag_name = release["tag_name"]
display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name
row_index = len(grid.get_children()) // 2
label = Gtk.Label(label=display_tag_name, xalign=0)
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
grid.attach(label, 0, row_index, 1, 1)
version_path = self.get_installed_path(display_tag_name)
is_installed = os.path.exists(version_path)
button = Gtk.Button(label=_("Remove") if is_installed else _("Download"))
button.connect("clicked", self.on_button_clicked, release)
button.set_size_request(120, -1)
grid.attach(button, 1, row_index, 1, 1)
def get_installed_path(self, tag_name):
tag_lower = tag_name.lower()
if STEAM_COMPATIBILITY_PATH.exists():
for folder in STEAM_COMPATIBILITY_PATH.iterdir():
folder_name_lower = folder.name.lower()
if folder_name_lower.endswith(tag_lower):
return folder
if tag_lower.startswith("proton-"):
if folder_name_lower.endswith(tag_lower[len("proton-"):]):
return folder
return STEAM_COMPATIBILITY_PATH / tag_name
def update_button(self, button, new_label):
button.set_label(new_label)
button.set_sensitive(True)
def on_button_clicked(self, widget, release):
tag_name = release["tag_name"]
if tag_name.startswith("EM-"):
tag_name = f"proton-{tag_name}"
version_path = self.get_installed_path(tag_name)
if os.path.exists(version_path):
self.on_remove_clicked(widget, release)
else:
self.progress_bar.set_visible(True)
self.progress_label.set_visible(True)
self.on_download_clicked(widget, release)
def disable_all_buttons(self):
for grid in (self.grid_ge, self.grid_em):
for child in grid.get_children():
if isinstance(child, Gtk.Button):
child.set_sensitive(False)
def enable_all_buttons(self):
for grid in (self.grid_ge, self.grid_em):
for child in grid.get_children():
if isinstance(child, Gtk.Button):
child.set_sensitive(True)
def on_download_clicked(self, widget, release):
self.disable_all_buttons()
for asset in release["assets"]:
if asset["name"].endswith((".tar.gz", ".tar.xz")):
self.download_and_extract(
asset["browser_download_url"],
asset["name"],
release["tag_name"],
widget
)
break
def download_and_extract(self, url, filename, tag_name, button):
button.set_label(_("Downloading..."))
display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name
self.progress_label.set_text(_("Downloading %s...") % display_tag_name)
self.progress_label.set_visible(True)
self.progress_bar.set_visible(True)
self.progress_bar.set_fraction(0)
button.set_sensitive(False)
while Gtk.events_pending():
Gtk.main_iteration_do(False)
if not os.path.exists(STEAM_COMPATIBILITY_PATH):
os.makedirs(STEAM_COMPATIBILITY_PATH)
response = requests.get(url, stream=True)
total_size = int(response.headers.get("content-length", 0))
downloaded_size = 0
tar_file_path = os.path.join(os.getcwd(), filename)
with open(tar_file_path, "wb") as file:
for data in response.iter_content(1024):
file.write(data)
downloaded_size += len(data)
progress = downloaded_size / total_size if total_size > 0 else 0
self.progress_bar.set_fraction(progress)
self.progress_bar.set_text(f"{int(progress * 100)}%")
while Gtk.events_pending():
Gtk.main_iteration_do(False)
file.flush()
os.fsync(file.fileno())
self.extract_tar_and_update_button(tar_file_path, tag_name, button)
def extract_tar_and_update_button(self, tar_file_path, tag_name, button):
button.set_label(_("Extracting..."))
display_tag_name = f"proton-{tag_name}" if tag_name.startswith("EM-") else tag_name
self.progress_label.set_text(_("Extracting %s...") % display_tag_name)
self.progress_bar.set_fraction(0)
self.progress_bar.set_text("0%")
while Gtk.events_pending():
Gtk.main_iteration_do(False)
mode = 'r:xz' if tar_file_path.endswith('.tar.xz') else 'r:gz'
try:
with tarfile.open(tar_file_path, mode) as tar:
total_members = len(tar.getmembers())
extracted_members = 0
temp_dir = os.path.join(STEAM_COMPATIBILITY_PATH, f"temp_{tag_name}")
os.makedirs(temp_dir, exist_ok=True)
for member in tar.getmembers():
tar.extract(member, path=temp_dir, filter="fully_trusted")
extracted_members += 1
progress = extracted_members / total_members
self.progress_bar.set_fraction(progress)
self.progress_bar.set_text(f"{int(progress * 100)}%")
while Gtk.events_pending():
Gtk.main_iteration_do(False)
extracted_dir = None
for item in os.listdir(temp_dir):
item_path = os.path.join(temp_dir, item)
if os.path.isdir(item_path):
extracted_dir = item_path
break
if extracted_dir:
final_dir = os.path.join(STEAM_COMPATIBILITY_PATH, os.path.basename(extracted_dir))
if os.path.exists(final_dir):
shutil.rmtree(final_dir)
shutil.move(extracted_dir, STEAM_COMPATIBILITY_PATH)
shutil.rmtree(temp_dir)
os.remove(tar_file_path)
self.update_button(button, _("Remove"))
self.progress_bar.set_visible(False)
self.progress_label.set_visible(False)
except Exception as e:
print(f"Error during extraction: {e}")
self.progress_label.set_text(_("Error during extraction"))
self.update_button(button, _("Download"))
finally:
self.enable_all_buttons()
button.set_sensitive(True)
def on_remove_clicked(self, widget, release):
version_path = self.get_installed_path(release["tag_name"])
if version_path and os.path.exists(version_path):
try:
shutil.rmtree(version_path)
self.update_button(widget, _("Download"))
except Exception:
pass
def main():
apply_dark_theme()
win = ProtonDownloader()
win.connect("destroy", Gtk.main_quit)
Gtk.main()
if __name__ == "__main__":
main()