diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2aaf6c60..643f779e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} commit_message: Update AUR package - pyinstaller-windows: + cx-freeze-windows: runs-on: windows-latest steps: - uses: actions/checkout@v2 @@ -55,25 +55,18 @@ jobs: python-version: '3.8' - name: Install python deps run: | - pip3 install pyinstaller setuptools wheel + pip3 install cx_Freeze setuptools wheel pip3 install -r requirements.txt - - name: Prepare - run: cp rare/__main__.py ./ - name: Build - run: pyinstaller - --icon=rare/styles/Logo.ico - --onefile - --name Rare - --add-data="Rare/languages/*;Rare/languages" - --add-data="Rare/Styles/*;Rare/Styles" - --windowed - __main__.py + run: python3 freeze.py bdist_msi + - name: Copy File + run: cp dist/*.msi Rare.msi - name: Upload files to GitHub uses: svenstaro/upload-release-action@2.2.1 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: dist/Rare.exe - asset_name: Rare.exe + file: Rare.msi + asset_name: Rare.msi tag: ${{ github.ref }} overwrite: true diff --git a/.gitignore b/.gitignore index 2547711d..af20f06c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ __pycache__ /.vscode /build /dist +/deb_dist +*.tar.gz /Rare.egg-info/ -/venv \ No newline at end of file +/venv diff --git a/README.md b/README.md index 6d31d84f..ff0486ab 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,14 @@ Install via `pip`. ## Run from source 1. Run `pip install -r requirements.txt` to get dependencies. If you use `pacman` you can run `sudo pacman --needed -S python-wheel python-setuptools python-pyqt5 python-qtawesome python-requests python-pillow` -2. For unix operating systems run `sh start.sh`. For windows run `set PYTHONPATH=%CD% && python Rare` +2. For unix operating systems run `sh start.sh`. For windows run `set PYTHONPATH=%CD% && python rare` ## Why Rare? -- Rare uses much less RAM than electron based apps such as [HeroicGL](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) and EpicGL which allows the games to run better. -- Rare supports all major platforms (Windows, Linux, macOS) unlike the alternatives. +- Rare only uses ~50MB of RAM which is much less than the electron based [HeroicGamesLauncher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) uses. +- Rare supports all major platforms (Windows, Linux, Mac) unlike the alternatives. + +**Note** Mac should work too, but I have no Mac and I can't test it. ## Features @@ -60,7 +62,7 @@ Install via `pip`. ## Planned Features - More Translations (Need help) - More Information about Games -More planned features are in projects +More planned features are in [projects](https://github.com/Dummerle/Rare/projects/1) ## Contributing There are more options to contribute. diff --git a/freeze.py b/freeze.py new file mode 100644 index 00000000..10fe3052 --- /dev/null +++ b/freeze.py @@ -0,0 +1,74 @@ +import sys + +from cx_Freeze import setup, Executable + +from rare import __version__ + +# Packages to include +python_packages = [] + +# Modules to include +python_modules = [] + +base = None +name = None +build_options = {} +build_exe_options = {} +shortcutName = None +shortcutDir = None +bdist_msi_options = None +src_files = [] +external_so_files = [] + +if sys.platform == 'win32': + base = 'Win32GUI' + name = 'Rare.exe' + shortcut_table = [ + ('DesktopShortcut', # Shortcut + 'DesktopFolder', # Directory + 'Rare', # Name + 'TARGETDIR', # Component + '[TARGETDIR]'+name, # Target + None, # Arguments + 'A gui for Legendary.', # Description + None, # Hotkey + None, # Icon + None, # IconIndex + None, # ShowCmd + 'TARGETDIR' # Working Directory + )] + msi_data = {"Shortcut": shortcut_table} + bdist_msi_options = {'data': msi_data, "all_users": True} + build_options["bdist_msi"] = bdist_msi_options +else: + name = 'Rare' + +src_files += [ + 'LICENSE', + 'README.md', + 'rare/styles/Logo.ico', +] + +# Dependencies are automatically detected, but it might need fine tuning. +build_exe_options["packages"] = python_packages +build_exe_options["include_files"] = src_files + external_so_files +build_exe_options["includes"] = python_modules +build_exe_options["excludes"] = ["setuptools", "tkinter", "pkg_resources"] + +# Set options +build_options["build_exe"] = build_exe_options + +setup(name = 'Rare', + version = __version__, + description = 'A gui for Legendary.', + options = build_options, + executables = [ + Executable('rare/__main__.py', + targetName=name, + icon='rare/styles/Logo.ico', + base=base, + shortcutName=shortcutName, + shortcutDir=shortcutDir, + ), + ], +) diff --git a/rare/__init__.py b/rare/__init__.py index 4a55d0b2..e826caf3 100644 --- a/rare/__init__.py +++ b/rare/__init__.py @@ -1,5 +1,5 @@ import os -__version__ = "1.3.0" +__version__ = "1.4.1" style_path = os.path.join(os.path.dirname(__file__), "styles/") lang_path = os.path.join(os.path.dirname(__file__), "languages/") diff --git a/rare/app.py b/rare/app.py index 5acebeb7..7863bd8d 100644 --- a/rare/app.py +++ b/rare/app.py @@ -1,7 +1,6 @@ import configparser import logging import os -import shutil import sys import time @@ -16,7 +15,7 @@ from rare.components.main_window import MainWindow from rare.components.tray_icon import TrayIcon from rare.utils.utils import get_lang, load_color_scheme -start_time = time.strftime('%y-%m-%d--%H:%M') # year-month-day-hour-minute +start_time = time.strftime('%y-%m-%d--%H-%M') # year-month-day-hour-minute file_name = os.path.expanduser(f"~/.cache/rare/logs/Rare_{start_time}.log") if not os.path.exists(os.path.dirname(file_name)): os.makedirs(os.path.dirname(file_name)) diff --git a/rare/components/tab_widget.py b/rare/components/tab_widget.py index a7d11f21..39e51a83 100644 --- a/rare/components/tab_widget.py +++ b/rare/components/tab_widget.py @@ -3,9 +3,14 @@ import webbrowser from PyQt5.QtCore import QSize, pyqtSignal from PyQt5.QtWidgets import QMenu, QTabWidget, QWidget, QWidgetAction from qtawesome import icon +from rare.utils import legendary_utils from custom_legendary.core import LegendaryCore + from rare.components.dialogs.install_dialog import InstallDialog + +from rare.components.dialogs.uninstall_dialog import UninstallDialog + from rare.components.tab_utils import TabBar, TabButtonWidget from rare.components.tabs.account import MiniWidget from rare.components.tabs.cloud_saves import SyncSaves @@ -64,6 +69,12 @@ class TabWidget(QTabWidget): # open download tab self.games_tab.default_widget.game_list.update_game.connect(lambda: self.setCurrentIndex(1)) + # uninstall + self.games_tab.game_info.info.uninstall_game.connect(self.uninstall_game) + + # imported + self.games_tab.import_widget.update_list.connect(self.game_imported) + if not offline: # Download finished self.downloadTab.finished.connect(self.dl_finished) @@ -87,6 +98,7 @@ class TabWidget(QTabWidget): self.tabBarClicked.connect(lambda x: self.games_tab.layout.setCurrentIndex(0) if x == 0 else None) self.setIconSize(QSize(25, 25)) + def install_game(self, app_name, disable_path=False): infos = InstallDialog(app_name, self.core, disable_path).get_information() @@ -102,16 +114,38 @@ class TabWidget(QTabWidget): self.setTabText(1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")) self.downloadTab.install_game(options) + def game_imported(self, app_name: str): + igame = self.core.get_installed_game(app_name) + if self.core.get_asset(app_name, True).build_version != igame.version: + self.downloadTab.add_update(igame) + downloads = len(self.downloadTab.dl_queue) + len(self.downloadTab.update_widgets.keys()) + self.setTabText(1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")) + self.games_tab.default_widget.game_list.update_list(app_name) + self.games_tab.layout.setCurrentIndex(0) + # Sync game and delete dc rpc def game_finished(self, app_name): self.delete_presence.emit() if self.core.get_game(app_name).supports_cloud_saves: self.cloud_saves.sync_game(app_name, True) + def uninstall_game(self, app_name): + game = self.core.get_game(app_name) + infos = UninstallDialog(game).get_information() + if infos == 0: + return + legendary_utils.uninstall(game.app_name, self.core, infos) + if app_name in self.downloadTab.update_widgets.keys(): + self.downloadTab.update_layout.removeWidget(self.downloadTab.update_widgets[app_name]) + self.downloadTab.update_widgets.pop(app_name) + downloads = len(self.downloadTab.dl_queue) + len(self.downloadTab.update_widgets.keys()) + self.setTabText(1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")) + self.downloadTab.update_text.setVisible(len(self.downloadTab.update_widgets) == 0) # Update gamelist and set text of Downlaods to "Downloads" + def dl_finished(self, update_list): - if update_list: - self.games_tab.default_widget.game_list.update_list() + if update_list[0]: + self.games_tab.default_widget.game_list.update_list(update_list[1]) downloads = len(self.downloadTab.dl_queue) + len(self.downloadTab.update_widgets.keys()) self.setTabText(1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")) diff --git a/rare/components/tabs/cloud_saves/sync_widget.py b/rare/components/tabs/cloud_saves/sync_widget.py index 46ccdd5a..889a7ee3 100644 --- a/rare/components/tabs/cloud_saves/sync_widget.py +++ b/rare/components/tabs/cloud_saves/sync_widget.py @@ -1,7 +1,7 @@ import os from logging import getLogger -from PyQt5.QtCore import QThread, pyqtSignal, Qt +from PyQt5.QtCore import QThread, pyqtSignal, Qt, QSettings from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QGroupBox from custom_legendary.core import LegendaryCore @@ -167,6 +167,11 @@ class SyncWidget(QGroupBox): self.layout.addStretch(1) self.setLayout(self.layout) + if self.res == SaveGameStatus.REMOTE_NEWER: + settings = QSettings() + if settings.value(f"{igame.app_name}/auto_sync_cloud", False, bool): + self.download() + def change_path(self): path = PathInputDialog("Select directory", "Select savepath. Warning: Do not change if you are not sure", self.igame.save_path).get_path() diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index 47c29fe9..1cbb3ebe 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -20,7 +20,7 @@ logger = getLogger("Download") class DownloadTab(QWidget): - finished = pyqtSignal(bool) + finished = pyqtSignal(tuple) thread: QThread dl_queue = [] @@ -75,18 +75,22 @@ class DownloadTab(QWidget): self.update_text.setVisible(len(updates) == 0) for igame in updates: - widget = UpdateWidget(core, igame, self) - self.update_layout.addWidget(widget) - self.update_widgets[igame.app_name] = widget - widget.update_signal.connect(self.update_game) - if QSettings().value("auto_update", False, bool): - self.update_game(igame.app_name, True) - widget.update_button.setDisabled(True) + + self.add_update(igame) self.layout.addStretch(1) self.setLayout(self.layout) + def add_update(self, igame: InstalledGame): + widget = UpdateWidget(self.core, igame, self) + self.update_layout.addWidget(widget) + self.update_widgets[igame.app_name] = widget + widget.update.connect(self.update_game) + if QSettings().value("auto_update", False, bool): + self.update_game(igame.app_name, True) + widget.update_button.setDisabled(True) + def update_dl_queue(self, dl_queue): self.dl_queue = dl_queue @@ -135,7 +139,7 @@ class DownloadTab(QWidget): # Information if not from_update: if not InstallInfoDialog(dl_size=analysis.dl_size, install_size=analysis.install_size).get_accept(): - self.finished.emit(False) + self.finished.emit(False, None) return if self.active_game is None: @@ -213,23 +217,25 @@ class DownloadTab(QWidget): # QMessageBox.information(self, "Info", "Download finished") logger.info("Download finished: " + self.active_game.app_title) + app_name = self.active_game.app_name + self.active_game = None + if self.dl_queue: - if self.dl_queue[0][1] == self.active_game.app_name: + if self.dl_queue[0][1] == app_name: self.dl_queue.pop(0) self.queue_widget.update_queue(self.dl_queue) - if self.active_game.app_name in self.update_widgets.keys(): - self.update_widgets[self.active_game.app_name].setVisible(False) - self.update_widgets.pop(self.active_game.app_name) + if app_name in self.update_widgets.keys(): + self.update_widgets[app_name].setVisible(False) + self.update_widgets.pop(app_name) if len(self.update_widgets) == 0: self.update_text.setVisible(True) - self.active_game = None - for i in self.update_widgets.values(): i.update_button.setDisabled(False) - self.finished.emit(True) + self.finished.emit((True, app_name)) + self.reset_infos() if len(self.dl_queue) != 0: @@ -243,7 +249,7 @@ class DownloadTab(QWidget): elif text == "stop": self.reset_infos() self.active_game = None - self.finished.emit(False) + self.finished.emit((False, None)) if self.dl_queue: self.start_installation(*self.dl_queue[0]) @@ -302,6 +308,7 @@ class UpdateWidget(QWidget): self.update_with_settings.clicked.connect(lambda: self.update_game(False)) self.layout.addWidget(self.update_button) self.layout.addWidget(self.update_with_settings) + self.layout.addWidget(QLabel(self.tr("Version: ") + self.game.version + " -> " + self.core.get_asset(self.game.app_name, True).build_version)) self.setLayout(self.layout) diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py index e425672a..7224f8ae 100644 --- a/rare/components/tabs/games/__init__.py +++ b/rare/components/tabs/games/__init__.py @@ -1,5 +1,6 @@ -from PyQt5.QtCore import QSettings, QSize -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QLineEdit, QPushButton, QStackedLayout, QLabel +from PyQt5.QtCore import QSettings, QSize, pyqtSignal +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QStackedLayout, \ + QLabel, QComboBox from qtawesome import icon from rare.components.tabs.games.game_info import InfoTabs @@ -36,8 +37,8 @@ class GameTab(QWidget): self.setLayout(self.layout) - def update_list(self): - self.default_widget.game_list.update_list(self.default_widget.head_bar.view.isChecked()) + def update_list(self, app_name=None): + self.default_widget.game_list.update_list(app_name) self.layout.setCurrentIndex(0) def show_uninstalled(self, app_name): @@ -62,11 +63,10 @@ class Games(QWidget): self.game_list = GameList(core, self, offline) self.head_bar.search_bar.textChanged.connect( - lambda: self.game_list.filter(self.head_bar.search_bar.text())) + lambda: self.game_list.search(self.head_bar.search_bar.text())) + + self.head_bar.filter_changed_signal.connect(self.game_list.filter) - self.head_bar.installed_only.stateChanged.connect(lambda: - self.game_list.installed_only( - self.head_bar.installed_only.isChecked())) self.layout.addWidget(self.head_bar) self.layout.addWidget(self.game_list) # self.layout.addStretch(1) @@ -81,14 +81,23 @@ class Games(QWidget): class GameListHeadBar(QWidget): + filter_changed_signal = pyqtSignal(str) + def __init__(self, parent): super(GameListHeadBar, self).__init__(parent=parent) self.layout = QHBoxLayout() - self.installed_only = QCheckBox(self.tr("Installed only")) + # self.installed_only = QCheckBox(self.tr("Installed only")) self.settings = QSettings() - self.installed_only.setChecked(self.settings.value("installed_only", False, bool)) - self.layout.addWidget(self.installed_only) + # self.installed_only.setChecked(self.settings.value("installed_only", False, bool)) + # self.layout.addWidget(self.installed_only) + self.filter = QComboBox() + self.filter.addItems([self.tr("All"), + self.tr("Installed only"), + self.tr("Offline Games"), + self.tr("32 Bit Games")]) + self.filter.currentIndexChanged.connect(self.filter_changed) + self.layout.addWidget(self.filter) self.layout.addStretch(1) self.import_game = QPushButton(icon("mdi.import", color="white"), self.tr("Import Game")) @@ -118,3 +127,6 @@ class GameListHeadBar(QWidget): self.layout.addWidget(self.refresh_list) self.setLayout(self.layout) + + def filter_changed(self, i): + self.filter_changed_signal.emit(["", "installed", "offline", "32bit"][i]) diff --git a/rare/components/tabs/games/game_info/__init__.py b/rare/components/tabs/games/game_info/__init__.py index e658e1c0..790d5dfb 100644 --- a/rare/components/tabs/games/game_info/__init__.py +++ b/rare/components/tabs/games/game_info/__init__.py @@ -59,7 +59,8 @@ class InfoTabs(QTabWidget): class GameInfo(QWidget, Ui_GameInfo): igame: InstalledGame game: Game - update_list = pyqtSignal() + uninstall_game = pyqtSignal(str) + update_list = pyqtSignal(str) verify_game = pyqtSignal(str) verify_threads = {} @@ -87,12 +88,8 @@ class GameInfo(QWidget, Ui_GameInfo): self.repair_button.clicked.connect(self.repair) def uninstall(self): - infos = UninstallDialog(self.game).get_information() - if infos == 0: - print("Cancel Uninstall") - return - legendary_utils.uninstall(self.game.app_name, self.core, infos) - self.update_list.emit() + self.uninstall_game.emit(self.game.app_name) + self.update_list.emit(self.game.app_name) def repair(self): repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{self.game.app_name}.repair') diff --git a/rare/components/tabs/games/game_list.py b/rare/components/tabs/games/game_list.py index 8627b42f..771beeb9 100644 --- a/rare/components/tabs/games/game_list.py +++ b/rare/components/tabs/games/game_list.py @@ -4,9 +4,10 @@ from logging import getLogger import psutil from PyQt5.QtCore import Qt, pyqtSignal, QSettings, QTimer from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import * +from PyQt5.QtWidgets import QScrollArea, QWidget, QLabel, QVBoxLayout, QStackedWidget from custom_legendary.core import LegendaryCore +from rare.components.tabs.games.game_widgets.base_installed_widget import BaseInstalledWidget from rare.components.tabs.games.game_widgets.installed_icon_widget import GameWidgetInstalled from rare.components.tabs.games.game_widgets.installed_list_widget import InstalledListWidget from rare.components.tabs.games.game_widgets.uninstalled_icon_widget import IconWidgetUninstalled @@ -63,93 +64,36 @@ class GameList(QStackedWidget): self.list_layout = QVBoxLayout() self.list_layout.addWidget(QLabel(self.info_text)) - IMAGE_DIR = self.settings.value("img_dir", os.path.expanduser("~/.cache/rare/images"), str) + self.IMAGE_DIR = self.settings.value("img_dir", os.path.expanduser("~/.cache/rare/images"), str) self.updates = [] self.widgets = {} + self.bit32 = [i.app_name for i in self.core.get_game_and_dlc_list(True, "Win32")[0]] + self.mac_games = [i.app_name for i in self.core.get_game_and_dlc_list(True, "Mac")[0]] self.installed = sorted(self.core.get_installed_list(), key=lambda x: x.title) # Installed Games for igame in self.installed: - if os.path.exists(f"{IMAGE_DIR}/{igame.app_name}/FinalArt.png"): - pixmap = QPixmap(f"{IMAGE_DIR}/{igame.app_name}/FinalArt.png") - elif os.path.exists(f"{IMAGE_DIR}/{igame.app_name}/DieselGameBoxTall.png"): - pixmap = QPixmap(f"{IMAGE_DIR}/{igame.app_name}/DieselGameBoxTall.png") - elif os.path.exists(f"{IMAGE_DIR}/{igame.app_name}/DieselGameBoxLogo.png"): - pixmap = QPixmap(f"{IMAGE_DIR}/{igame.app_name}/DieselGameBoxLogo.png") - else: - logger.warning(f"No Image found: {igame.title}") - pixmap = None - - if pixmap.isNull(): - logger.info(igame.title + " has a corrupt image.") - download_image(igame, force=True) - pixmap = QPixmap(f"{IMAGE_DIR}/{igame.app_name}/DieselGameBoxTall.png") - - icon_widget = GameWidgetInstalled(igame, self.core, pixmap, self.offline) + icon_widget, list_widget = self.add_installed_widget(igame) self.icon_layout.addWidget(icon_widget) - - list_widget = InstalledListWidget(igame, self.core, pixmap, self.offline) self.list_layout.addWidget(list_widget) - icon_widget.show_info.connect(self.show_game_info.emit) - list_widget.show_info.connect(self.show_game_info.emit) - - icon_widget.launch_signal.connect(self.launch) - icon_widget.finish_signal.connect(self.finished) - icon_widget.update_list.connect(lambda: self.update_list(self.settings.value("icon_view", True, bool))) - list_widget.launch_signal.connect(self.launch) - list_widget.finish_signal.connect(self.finished) - list_widget.update_list.connect(lambda: self.update_list(self.settings.value("icon_view", True, bool))) - - if icon_widget.update_available: - self.updates.append(igame) - - self.widgets[igame.app_name] = (icon_widget, list_widget) - active, pid = self.check_for_active_game(igame) - if active: - # Only one game works: Workaround for Satisfactory EA and Exp. - self.launch(igame.app_name) - icon_widget.game_running = True - list_widget.game_running = True - self.active_game = (igame.app_name, pid) - self.timer = QTimer() - self.timer.timeout.connect(self.is_finished) - self.timer.start(10000) - - self.uninstalled_games = [] + self.uninstalled_names = [] installed = [i.app_name for i in self.core.get_installed_list()] # get Uninstalled games games, self.dlcs = self.core.get_game_and_dlc_list() for game in sorted(games, key=lambda x: x.app_title): if not game.app_name in installed: - self.uninstalled_games.append(game) + self.uninstalled_names.append(game) # add uninstalled games - for igame in self.uninstalled_games: - if os.path.exists(f"{IMAGE_DIR}/{igame.app_name}/UninstalledArt.png"): - pixmap = QPixmap(f"{IMAGE_DIR}/{igame.app_name}/UninstalledArt.png") - if pixmap.isNull(): - logger.info(igame.app_title + " has a corrupt image.") - download_image(igame, force=True) - pixmap = QPixmap(f"{IMAGE_DIR}/{igame.app_name}/UninstalledArt.png") - else: - logger.warning(f"No Image found: {igame.app_title}") - download_image(igame, force=True) - pixmap = QPixmap(f"{IMAGE_DIR}/{igame.app_name}/UninstalledArt.png") - - icon_widget = IconWidgetUninstalled(igame, self.core, pixmap) - icon_widget.show_uninstalled_info.connect(self.install) - - list_widget = ListWidgetUninstalled(self.core, igame, pixmap) - list_widget.show_uninstalled_info.connect(self.install) + for game in self.uninstalled_games: + icon_widget, list_widget = self.add_uninstalled_widget(game) self.icon_layout.addWidget(icon_widget) self.list_layout.addWidget(list_widget) - self.widgets[igame.app_name] = (icon_widget, list_widget) - self.icon_parent_layout.addLayout(self.icon_layout) self.icon_parent_layout.addStretch(1) self.list_layout.addStretch(1) @@ -165,8 +109,81 @@ class GameList(QStackedWidget): if not icon_view: self.setCurrentIndex(1) - if self.settings.value("installed_only", False, bool): - self.installed_only(True) + if filter_games := self.settings.value("filter", "", str): + self.filter(filter_games) + + def add_uninstalled_widget(self, game): + if os.path.exists(f"{self.IMAGE_DIR}/{game.app_name}/UninstalledArt.png"): + pixmap = QPixmap(f"{self.IMAGE_DIR}/{game.app_name}/UninstalledArt.png") + + if pixmap.isNull(): + logger.info(game.app_title + " has a corrupt image.") + download_image(game, force=True) + pixmap = QPixmap(f"{self.IMAGE_DIR}/{game.app_name}/UninstalledArt.png") + else: + logger.warning(f"No Image found: {game.app_title}") + download_image(game, force=True) + pixmap = QPixmap(f"{self.IMAGE_DIR}/{game.app_name}/UninstalledArt.png") + + icon_widget = IconWidgetUninstalled(game, self.core, pixmap) + icon_widget.install_game.connect(self.install) + + list_widget = ListWidgetUninstalled(self.core, game, pixmap) + list_widget.install_game.connect(self.install) + + self.widgets[game.app_name] = (icon_widget, list_widget) + + return icon_widget, list_widget + + def add_installed_widget(self, igame): + if os.path.exists(f"{self.IMAGE_DIR}/{igame.app_name}/FinalArt.png"): + pixmap = QPixmap(f"{self.IMAGE_DIR}/{igame.app_name}/FinalArt.png") + elif os.path.exists(f"{self.IMAGE_DIR}/{igame.app_name}/DieselGameBoxTall.png"): + pixmap = QPixmap(f"{self.IMAGE_DIR}/{igame.app_name}/DieselGameBoxTall.png") + elif os.path.exists(f"{self.IMAGE_DIR}/{igame.app_name}/DieselGameBoxLogo.png"): + pixmap = QPixmap(f"{self.IMAGE_DIR}/{igame.app_name}/DieselGameBoxLogo.png") + else: + logger.warning(f"No Image found: {igame.title}") + pixmap = None + + if pixmap.isNull(): + logger.info(igame.title + " has a corrupt image.") + download_image(igame, force=True) + pixmap = QPixmap(f"{self.IMAGE_DIR}/{igame.app_name}/DieselGameBoxTall.png") + + icon_widget = GameWidgetInstalled(igame, self.core, pixmap, self.offline) + # self.icon_layout.addWidget(icon_widget) + + list_widget = InstalledListWidget(igame, self.core, pixmap, self.offline) + # self.list_layout.addWidget(list_widget) + + self.widgets[igame.app_name] = (icon_widget, list_widget) + + icon_widget.show_info.connect(self.show_game_info.emit) + list_widget.show_info.connect(self.show_game_info.emit) + + icon_widget.launch_signal.connect(self.launch) + icon_widget.finish_signal.connect(self.finished) + icon_widget.update_list.connect(self.update_list) + list_widget.launch_signal.connect(self.launch) + list_widget.finish_signal.connect(self.finished) + list_widget.update_list.connect(self.update_list) + + if icon_widget.update_available: + self.updates.append(igame) + + active, pid = self.check_for_active_game(igame) + if active: + # Only one game works: Workaround for Satisfactory EA and Exp. + self.launch(igame.app_name) + icon_widget.game_running = True + list_widget.game_running = True + self.active_game = (igame.app_name, pid) + self.timer = QTimer() + self.timer.timeout.connect(self.is_finished) + self.timer.start(10000) + + return icon_widget, list_widget def is_finished(self): if psutil.pid_exists(self.active_game[1]): @@ -226,7 +243,7 @@ class GameList(QStackedWidget): self.widgets[app_name][1].launch_button.setDisabled(True) self.widgets[app_name][1].launch_button.setText(self.tr("Game running")) - def filter(self, text: str): + def search(self, text: str): for t in self.widgets.values(): for w in t: if text.lower() in w.game.app_title.lower() + w.game.app_name.lower(): @@ -234,31 +251,151 @@ class GameList(QStackedWidget): else: w.setVisible(False) - def installed_only(self, i_o: bool): + def filter(self, filter="installed"): for t in self.widgets.values(): for w in t: - w.setVisible(not (not self.core.is_installed(w.game.app_name) and i_o)) - self.settings.setValue("installed_only", i_o) + if filter == "installed": + w.setVisible(self.core.is_installed(w.game.app_name)) + elif filter == "offline": + if self.core.is_installed(w.game.app_name): + w.setVisible(w.igame.can_run_offline) + else: + w.setVisible(False) + elif filter == "32bit": + w.setVisible(w.game.app_name in self.bit32) + elif filter == "mac": + w.setVisible(w.game.app_name in self.mac_games) + else: + # All visible + w.setVisible(True) + self.settings.setValue("filter", filter) - def update_list(self, icon_view=True): - self.settings.setValue("icon_view", icon_view) + def update_list(self, app_name=None): + # self.settings.setValue("icon_view", icon_view) + if app_name: + if widgets := self.widgets.get(app_name): - uninstalled_games = [] - installed = [i.app_name for i in self.core.get_installed_list()] - # get Uninstalled games - games = self.core.get_game_list(True) - for game in sorted(games, key=lambda x: x.app_title): - if not game.app_name in installed: - uninstalled_games.append(game.app_name) + # from update + if self.core.is_installed(widgets[0].game.app_name) and isinstance(widgets[0], BaseInstalledWidget): + igame = self.core.get_installed_game(app_name) + for w in widgets: + w.igame = igame + w.update_available = self.core.get_asset(w.game.app_name, True).build_version != igame.version + widgets[0].info_label.setText("") + widgets[0].info_text = "" + # new installed + elif self.core.is_installed(widgets[0].game.app_name) and not isinstance(widgets[0], BaseInstalledWidget): + self.widgets.pop(widgets[0].game.app_name) - # get new uninstalled/ installed games that changed - new_installed_games = list(set(installed) - set([i.app_name for i in self.installed])) - new_uninstalled_games = list(set(uninstalled_games) - set([i.app_name for i in self.uninstalled_games])) + # QWidget().setLayout(self.icon_layout) + icon_layout = FlowLayout() + + # QWidget().setLayout(self.list_layout) + list_layout = QVBoxLayout() + + igame = self.core.get_installed_game(app_name) + self.add_installed_widget(igame) + + for igame in sorted(self.core.get_installed_list(), key=lambda x: x.title): + i_widget, l_widget = self.widgets[igame.app_name] + icon_layout.addWidget(i_widget) + list_layout.addWidget(l_widget) + + self.uninstalled_names = [] + installed_names = [i.app_name for i in self.core.get_installed_list()] + # get Uninstalled games + games, self.dlcs = self.core.get_game_and_dlc_list() + for game in sorted(games, key=lambda x: x.app_title): + if not game.app_name in installed_names: + self.uninstalled_names.append(game) + for game in self.uninstalled_names: + i_widget, list_widget = self.widgets[game.app_name] + icon_layout.addWidget(i_widget) + list_layout.addWidget(list_widget) + + QWidget().setLayout(self.icon_layout) + QWidget().setLayout(self.list_layout) + + self.icon_widget = QWidget() + self.list_widget = QWidget() + + self.icon_widget.setLayout(icon_layout) + self.list_widget.setLayout(list_layout) + + self.list_scrollarea.setWidget(QWidget()) + self.icon_scrollarea.setWidget(QWidget()) + + self.icon_scrollarea.setWidget(self.icon_widget) + self.list_scrollarea.setWidget(self.list_widget) + + self.icon_layout = icon_layout + self.list_layout = list_layout + + self.icon_widget.setLayout(self.icon_layout) + self.list_widget.setLayout(self.list_layout) + + self.update() + + # uninstalled + elif not self.core.is_installed(widgets[0].game.app_name) and isinstance(widgets[0], BaseInstalledWidget): + self.list_layout.removeWidget(widgets[1]) + self.icon_layout.removeWidget(widgets[0]) + + self.widgets.pop(app_name) + + game = self.core.get_game(app_name, True) + self.add_uninstalled_widget(game) + + self.icon_layout.addWidget(self.widgets[app_name][0]) + self.list_layout.addWidget(self.widgets[app_name][1]) + else: + installed_names = [i.app_name for i in self.core.get_installed_list()] + # get Uninstalled games + uninstalled_games = [] + games = self.core.get_game_list(True) + for game in sorted(games, key=lambda x: x.app_title): + if not game.app_name in installed_names: + uninstalled_games.append(game.app_name) + + new_installed_games = list(set(installed_names) - set([i.app_name for i in self.installed])) + new_uninstalled_games = list(set(uninstalled_games) - set([i.app_name for i in self.uninstalled_names])) + + if (not new_uninstalled_games) and (not new_installed_games): + return + + if new_installed_games: + for name in new_installed_games: + self.icon_layout.removeWidget(self.widgets[app_name][0]) + self.list_layout.removeWidget(self.widgets[app_name][1]) + + self.widgets.pop(name) + + igame = self.core.get_installed_game(name) + self.add_installed_widget(igame) + + for name in new_uninstalled_games: + self.icon_layout.removeWidget(self.widgets[app_name][0]) + self.list_layout.removeWidget(self.widgets[app_name][1]) + + self.widgets.pop(name) + + game = self.core.get_game(name, True) + self.add_uninstalled_widget(game) + + for igame in sorted(self.core.get_installed_list(), key=lambda x: x.title): + i_widget, list_widget = self.widgets[igame.app_name] + + self.icon_layout.addWidget(i_widget) + self.list_layout.addWidget(list_widget) + + # get Uninstalled games + games, self.dlcs = self.core.get_game_and_dlc_list() + for game in sorted(games, key=lambda x: x.app_title): + if not game.app_name in installed_names: + self.uninstalled_names.append(game) + for name in uninstalled_games: + i_widget, list_widget = self.widgets[name] + self.icon_layout.addWidget(i_widget) + self.list_layout.addWidget(list_widget) - if not new_uninstalled_games and not new_installed_games: - return - self.removeWidget(self.icon_scrollarea) - self.removeWidget(self.list_scrollarea) - self.init_ui(icon_view) - self.update() diff --git a/rare/components/tabs/games/game_widgets/base_installed_widget.py b/rare/components/tabs/games/game_widgets/base_installed_widget.py index acd5a438..db8f9650 100644 --- a/rare/components/tabs/games/game_widgets/base_installed_widget.py +++ b/rare/components/tabs/games/game_widgets/base_installed_widget.py @@ -147,4 +147,4 @@ class BaseInstalledWidget(QGroupBox): print("Cancel Uninstall") return legendary_utils.uninstall(self.game.app_name, self.core, infos) - self.update_list.emit() + self.update_list.emit(self.game.app_name) diff --git a/rare/components/tabs/games/game_widgets/installed_icon_widget.py b/rare/components/tabs/games/game_widgets/installed_icon_widget.py index 26051a0c..e78e753b 100644 --- a/rare/components/tabs/games/game_widgets/installed_icon_widget.py +++ b/rare/components/tabs/games/game_widgets/installed_icon_widget.py @@ -14,7 +14,7 @@ logger = getLogger("GameWidgetInstalled") class GameWidgetInstalled(BaseInstalledWidget): - update_list = pyqtSignal() + update_list = pyqtSignal(str) show_info = pyqtSignal(str) update_game = pyqtSignal() diff --git a/rare/components/tabs/games/import_widget.py b/rare/components/tabs/games/import_widget.py index e2e8f7c8..ddb8978c 100644 --- a/rare/components/tabs/games/import_widget.py +++ b/rare/components/tabs/games/import_widget.py @@ -16,7 +16,7 @@ logger = getLogger("Import") class ImportWidget(QWidget): - update_list = pyqtSignal() + update_list = pyqtSignal(str) def __init__(self, core: LegendaryCore, parent): super(ImportWidget, self).__init__(parent=parent) @@ -119,7 +119,7 @@ class ImportWidget(QWidget): self.core.get_installed_game(app_name).title)) self.app_name_input.setText("") - self.update_list.emit() + self.update_list.emit(app_name) else: logger.warning("Failed to import" + app_name) self.info_label.setText(self.tr("Failed to import {}").format(app_name)) diff --git a/rare/languages/de.ts b/rare/languages/de.ts index f80c05fe..3d692ad8 100644 --- a/rare/languages/de.ts +++ b/rare/languages/de.ts @@ -1362,82 +1362,83 @@ Installationsgröße: {} GB RareSettings - + Rare settings Rare Einstellungen - + Save Speichern - + Image Directory Ordner für Bilder + Language Sprache - + Restart Application to activate changes Starte die App neu um die Änderungen zu aktivieren - + Confirm launch of game Start des Spiels bestätigen - + Exit to System Tray Icon Beim verlassen auf das System Tray Icon minimieren - + Hide to System Tray Icon In das System Tray Icon minimieren - + Auto sync with cloud Speicherstände automatisch mit der Cloud synchronisieren - + Sync with cloud Automatisch Synchronisieren - + Save size Größe Speichern - + Save size of window after restart Die Fenstergröße nach dem Beenden speichern - + Automatically update Games on startup Spiele automatisch updaten - + Auto updates Automatische Updates - + Show Notifications after Downloads Benachrichtigung nach Abschluss des Downloads anzeigen - + Show notification Benachrichtigung anzeigen @@ -1526,6 +1527,11 @@ Installationsgröße: {} GB Behavior Optionen + + + Open Log directory + + SyncSaves @@ -1643,22 +1649,22 @@ Installationsgröße: {} GB Pfad ändern - + Uploading... Hochladen... - + Upload finished Hochladen abgeschlossen - + Downloading... Runterladen... - + Download finished Download abgeschlossen diff --git a/rare/utils/legendary_utils.py b/rare/utils/legendary_utils.py index 958a0a2e..dac21139 100644 --- a/rare/utils/legendary_utils.py +++ b/rare/utils/legendary_utils.py @@ -61,6 +61,12 @@ def uninstall(app_name: str, core: LegendaryCore, options=None): if os.path.exists(os.path.expanduser(f"~/.local/share/applications/{igame.title}.desktop")): os.remove(os.path.expanduser(f"~/.local/share/applications/{igame.title}.desktop")) + elif os.name == "nt": + if os.path.exists(os.path.expanduser(f"~/Desktop/{igame.title.split(':')[0]}.lnk")): + os.remove(os.path.expanduser(f"~/Desktop/{igame.title.split(':')[0]}.lnk")) + elif os.path.exists(os.path.expandvars(f"%appdata%/Microsoft/Windows/Start Menu/{igame.title.split(':')[0]}.lnk")): + os.remove(os.path.expandvars(f"%appdata%/Microsoft/Windows/Start Menu/{igame.title.split(':')[0]}.lnk")) + try: # Remove DLC first so directory is empty when game uninstall runs dlcs = core.get_dlc_for_game(app_name) diff --git a/rare/utils/utils.py b/rare/utils/utils.py index 5c2a5fbc..c09c4f1a 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -14,12 +14,13 @@ if os.name == "nt": from win32com.client import Dispatch from rare import lang_path, style_path +# Mac not supported + from custom_legendary.core import LegendaryCore logger = getLogger("Utils") s = QSettings("Rare", "Rare") IMAGE_DIR = s.value("img_dir", os.path.expanduser("~/.cache/rare/images"), type=str) -logger.info("IMAGE DIRECTORY: " + IMAGE_DIR) def download_images(signal: pyqtSignal, core: LegendaryCore): @@ -246,7 +247,8 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): "Terminal=false\n" "StartupWMClass=rare-game\n" ) - os.chmod(os.path.expanduser(f"~/Desktop/{igame.title}.desktop"), 0o755) + desktop_file.close() + os.chmod(os.path.expanduser(f"{path}{igame.title}.desktop"), 0o755) # Windows elif os.name == "nt": @@ -261,7 +263,12 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): target = os.path.abspath(sys.argv[0]) # Name of link file - linkName = igame.title + '.lnk' + + linkName = igame.title + for c in r'<>?":|\/*': + linkName.replace(c, "") + + linkName = linkName.strip() + '.lnk' # Path to location of link file pathLink = os.path.join(target_folder, linkName) @@ -279,4 +286,5 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): img.save(icon + ".ico") logger.info("Create Icon") shortcut.IconLocation = os.path.join(icon + ".ico") + shortcut.save() diff --git a/requirements.txt b/requirements.txt index 1dfa98c3..67376a60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ PyQt5 QtAwesome notify-py psutil -pypresence \ No newline at end of file +pypresence +pywin32; platform_system == "Windows" diff --git a/setup.py b/setup.py index 87eb2725..456839c3 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +import os + import setuptools from rare import __version__ as version @@ -5,6 +7,21 @@ from rare import __version__ as version with open("README.md", "r") as fh: long_description = fh.read() +requirements = [ + "requests<3.0", + "pillow", + "setuptools", + "wheel", + "PyQt5", + "QtAwesome", + "notify-py", + "psutil", + "pypresence" + ] + +if os.name == "nt": + requirements.append("pywin32") + setuptools.setup( name="Rare", version=version, @@ -25,15 +42,5 @@ setuptools.setup( ], python_requires=">=3.8", entry_points=dict(console_scripts=["rare=rare.__main__:main"]), - install_requires=[ - "requests<3.0", - "pillow", - "setuptools", - "wheel", - "PyQt5", - "QtAwesome", - "notify-py", - "psutil", - "pypresence" - ], + install_requires=requirements, )