diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 00000000..5e3d8880 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,44 @@ + +name: "Checks" + + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'rare/**' + pull_request: + types: + - opened + - reopened + - synchronize + branches: + - main + paths: + - 'rare/**' + + +jobs: + pylint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install Test Dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Install Target Dependencies + run: | + pip3 install -r requirements.txt + pip3 install -r requirements-presence.txt + - name: Analysis with pylint + run: | + pylint -E rare --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5 diff --git a/.github/workflows/tests.yml b/.github/workflows/release-tests.yml similarity index 52% rename from .github/workflows/tests.yml rename to .github/workflows/release-tests.yml index dde4f957..9f3cdb4c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/release-tests.yml @@ -1,30 +1,12 @@ -name: "Test" +name: "Release Tests" + on: workflow_dispatch: -jobs: - pylint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - name: Set up Python 3.10 - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - pip install -r requirements.txt - pip install pypresence - - name: Analysing the code with pylint - run: | - pylint -E rare --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5 +jobs: deb-package: runs-on: ubuntu-latest steps: @@ -46,7 +28,8 @@ jobs: makedeb -d mv *.deb Rare.deb - - uses: actions/upload-artifact@v2 + - name: Upload to Artifacts + uses: actions/upload-artifact@v2 with: name: Rare.deb path: build/Rare.deb @@ -72,11 +55,55 @@ jobs: appimage-builder --skip-test mv Rare-*.AppImage Rare.AppImage - - uses: actions/upload-artifact@v2 + - name: Upload to Artifacts + uses: actions/upload-artifact@v2 with: name: Rare.AppImage path: Rare.AppImage + nuitka: + runs-on: "windows-latest" + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install build dependencies + run: pip3 install nuitka ordered-set + - name: Install target dependencies + run: | + pip3 install -r requirements.txt + pip3 install -r requirements-presence.txt + - name: Version + id: version + run: | + git fetch --prune --unshallow + echo "::set-output name=tag_offset::$(git describe --long --tags)" + echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" + - name: Build + run: >- + python -m nuitka + --assume-yes-for-downloads + --follow-imports --prefer-source-code --mingw64 --lto=no --jobs=2 --static-libpython=no --standalone + --enable-plugin=anti-bloat --enable-plugin=pyqt5 --show-anti-bloat-changes --nofollow-import-to="*.tests" + --nofollow-import-to="*.distutils" --include-package-data=qtawesome + --include-data-dir=rare\resources\images=rare\resources\images + --include-data-files=rare\resources\languages=rare\resources\languages="*.qm" + --windows-icon-from-ico=rare\resources\images\Rare.ico + --windows-company-name=Rare --windows-product-name=Rare --windows-file-description=rare.exe + --windows-file-version=0.0.0.0 + --windows-product-version=0.0.0.0 + --windows-disable-console + rare + + - name: Upload to Artifacts + uses: actions/upload-artifact@v2 + with: + name: Rare-Windows-${{ steps.version.outputs.tag_offset }} + path: rare.dist + cx_freeze: runs-on: "windows-latest" steps: @@ -85,19 +112,28 @@ jobs: submodules: true - uses: actions/setup-python@v2 with: - python-version: '3.10' - - name: Dependencies - run: pip3 install -r requirements.txt - - name: cx_freeze + python-version: '3.9' + - name: Install Build Dependencies run: pip3 install --upgrade cx_freeze wheel - - name: pypresence - run: pip3 install pypresence + - name: Install Target Dependencies + run: | + pip3 install -r requirements.txt + pip3 install -r requirements-presence.txt + - name: Version + id: version + run: | + git fetch --prune --unshallow + echo "::set-output name=tag_offset::$(git describe --long --tags)" + echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" - name: Build - run: python freeze.py bdist_msi + run: | + python freeze.py bdist_msi + mv dist/*.msi dist/Rare-Windows.msi - - uses: actions/upload-artifact@v2 + - name: Upload to Artifacts + uses: actions/upload-artifact@v2 with: - name: Rare-Windows.msi + name: Rare-Windows-${{ steps.version.outputs.tag_offset }}.msi path: dist/*.msi mac_os: @@ -120,8 +156,12 @@ jobs: run: mv rare/__main__.py __main__.py - name: run pyinstaller - run: | - pyinstaller -F --name Rare --add-data "rare/resources/languages/*:rare/resources/languages" --add-data "rare/resources/images/*:rare/resources/images/" --windowed --icon rare/resources/images/Rare.icns --hidden-import=legendary __main__.py + run: >- + pyinstaller -F --name Rare + --add-data "rare/resources/languages/*:rare/resources/languages" + --add-data "rare/resources/images/*:rare/resources/images/" + --windowed --icon rare/resources/images/Rare.icns + --hidden-import=legendary __main__.py - name: create dmg run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 193af825..391878ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,12 @@ -name: New Release + +name: "Release" + on: release: types: [ published ] + jobs: pypi-deploy: if: "!github.event.release.prerelease" @@ -48,7 +51,7 @@ jobs: makedeb -d mv *.deb Rare.deb - - name: Upload files to GitHub + - name: Upload to Releases uses: svenstaro/upload-release-action@2.2.1 with: repo_token: ${{ secrets.GITHUB_TOKEN }} @@ -79,7 +82,7 @@ jobs: mv Rare-*.AppImage Rare.AppImage mv Rare-*.AppImage.zsync Rare.AppImage.zsync - - name: Upload AppImage to GitHub + - name: Upload to Releases uses: svenstaro/upload-release-action@2.2.1 with: repo_token: ${{ secrets.GITHUB_TOKEN }} @@ -96,6 +99,49 @@ jobs: tag: ${{ github.ref }} overwrite: true + nuitka: + runs-on: "windows-latest" + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Install build dependencies + run: pip3 install nuitka ordered-set + - name: Install target dependencies + run: | + pip3 install -r requirements.txt + pip3 install -r requirements-presence.txt + - name: Build + run: >- + python -m nuitka + --assume-yes-for-downloads + --follow-imports --prefer-source-code --mingw64 --lto=no --jobs=2 --static-libpython=no --standalone + --enable-plugin=anti-bloat --enable-plugin=pyqt5 --show-anti-bloat-changes --nofollow-import-to="*.tests" + --nofollow-import-to="*.distutils" --include-package-data=qtawesome + --include-data-dir=rare\resources\images=rare\resources\images + --include-data-files=rare\resources\languages=rare\resources\languages="*.qm" + --windows-icon-from-ico=rare\resources\images\Rare.ico + --windows-company-name=Rare --windows-product-name=Rare --windows-file-description=rare.exe + --windows-file-version=${{ github.event.release.tag_name }} + --windows-product-version=${{ github.event.release.tag_name }} + --windows-disable-console + rare + - name: Compress + run: | + python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')" + + - name: Upload to Releases + uses: svenstaro/upload-release-action@2.2.1 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: Rare-Windows.zip + asset_name: Rare-Windows-${{ github.event.release.tag_name }}.zip + tag: ${{ github.ref }} + overwrite: true + cx_freeze: runs-on: "windows-latest" steps: @@ -104,24 +150,27 @@ jobs: submodules: true - uses: actions/setup-python@v2 with: - python-version: '3.8' - - name: Dependencies - run: pip3 install -r requirements.txt - - name: cx_freeze + python-version: '3.9' + - name: Install Build Dependencies run: pip3 install --upgrade cx_freeze wheel + - name: Install Target Dependencies + run: | + pip3 install -r requirements.txt + pip3 install -r requirements-presence.txt - name: Build - run: python freeze.py bdist_msi + run: | + python freeze.py bdist_msi + mv dist/*.msi dist/Rare-Windows.msi - - name: Rename File - run: mv dist/*.msi dist/Rare.msi - - name: Upload to GitHub + - name: Upload to Releases uses: svenstaro/upload-release-action@2.2.1 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: dist/Rare.msi - asset_name: Rare-${{ github.event.release.tag_name }}.msi + file: dist/Rare-Windows.msi + asset_name: Rare-Windows-${{ github.event.release.tag_name }}.msi tag: ${{ github.ref }} overwrite: true + mac_os: runs-on: macos-latest steps: @@ -142,8 +191,12 @@ jobs: run: mv rare/__main__.py __main__.py - name: run pyinstaller - run: | - pyinstaller -F --name Rare --add-data "rare/resources/languages/*:rare/resources/languages" --add-data "rare/resources/images/*:rare/resources/images/" --windowed --icon rare/resources/images/Rare.icns --hidden-import=legendary __main__.py + run: >- + pyinstaller -F --name Rare + --add-data "rare/resources/languages/*:rare/resources/languages" + --add-data "rare/resources/images/*:rare/resources/images/" + --windowed --icon rare/resources/images/Rare.icns + --hidden-import=legendary __main__.py - name: create dmg run: | diff --git a/misc/nuitka_rare.bat b/misc/nuitka_rare.bat new file mode 100644 index 00000000..aeb701a1 --- /dev/null +++ b/misc/nuitka_rare.bat @@ -0,0 +1 @@ +python -m nuitka --assume-yes-for-downloads --follow-imports --prefer-source-code --mingw64 --lto=no --jobs=2 --static-libpython=no --standalone --enable-plugin=anti-bloat --enable-plugin=pyqt5 --show-anti-bloat-changes --nofollow-import-to="*.tests" --nofollow-import-to="*.distutils" --include-package-data=qtawesome --include-data-dir=rare\resources\images=rare\resources\images --include-data-files=rare\resources\languages=rare\resources\languages="*.qm" --windows-icon-from-ico=rare\resources\images\Rare.ico --windows-company-name=Rare --windows-product-name=Rare --windows-file-description=rare.exe --windows-file-version=1.9.0 --windows-product-version=1.9.0 --windows-disable-console rare diff --git a/misc/pip_upgrade_venv.py b/misc/pip_upgrade_venv.py new file mode 100644 index 00000000..47abb5c0 --- /dev/null +++ b/misc/pip_upgrade_venv.py @@ -0,0 +1,5 @@ +import pkg_resources +from subprocess import call + +for dist in pkg_resources.working_set: + call("python -m pip install --upgrade " + dist.project_name, shell=True) diff --git a/pyproject.toml b/pyproject.toml index 45cd2e75..5f28b09e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,24 +33,36 @@ pywebview = [ legendary-gl = "^0.20.28" typing-extensions = "^4.3.0" - [tool.poetry.scripts] start = "rare.__main__:main" - - [tool.poetry.dev-dependencies] Nuitka = "^1.0.6" pylint = "^2.15.0" black = "^22.6.0" #[build-system] -#requires = ["setuptools>=42"z, "wheel", "nuitka", "toml"] +#requires = ["setuptools>=42", "wheel", "nuitka", "toml"] #build-backend = "nuitka.distutils.Build" - [nuitka] -show-scons = true -enable-plugin = ["pyqt5", "anti-bloat"] +assume-yes-for-downloads = true +follow-imports = true +prefer-source-code = true +mingw64 = true +lto = true +static-libpython = false +standalone = true +show-scons = false +enable-plugin = ["anti-bloat", "pyqt5"] show-anti-bloat-changes = true nofollow-import-to = ["*.tests", "*.distutils"] +include-package-data = "qtawesome" +include-data-dir = "rare/resources/images=rare/resources/images" +include-data-files = "rare/resources/languages=rare/resources/laguanges=*.qm" +windows-icon-from-ico = "rare/resources/images/Rare.ico" +windows-company-name = "Rare" +windows-product-name = "Rare" +windows-file-version = "1.9.0" +windows-product-version = "1.9.0" +windows-disable-console = true diff --git a/rare/__main__.py b/rare/__main__.py index d782c122..5a9348d6 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -117,4 +117,12 @@ if __name__ == "__main__": if "__compiled__" not in globals(): sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute())) + # If we are on Windows, and we are in a "compiled" GUI application form + # stdout (and stderr?) will be None. So to avoid `'NoneType' object has no attribute 'write'` + # errors, redirect both of them to devnull + if os.name == "nt" and (getattr(sys, "frozen", False) or ("__compiled__" in globals())): + f = open(os.devnull, 'w') + sys.stdout = f + sys.stderr = f + main() diff --git a/rare/app.py b/rare/app.py index cee3e06f..f333bcdf 100644 --- a/rare/app.py +++ b/rare/app.py @@ -20,7 +20,7 @@ from rare.shared import ( ArgumentsSingleton, ) from rare.shared.rare_core import RareCore -from rare.utils import legendary_utils, config_helper, paths +from rare.utils import config_helper, paths from rare.widgets.rare_app import RareApp logger = logging.getLogger("Rare") @@ -60,23 +60,23 @@ class App(RareApp): self.load_translator(lang) # set Application name for settings - self.mainwindow: Optional[MainWindow] = None + self.main_window: Optional[MainWindow] = None self.launch_dialog: Optional[LaunchDialog] = None - self.timer = QTimer() + self.timer: Optional[QTimer] = None # launch app self.launch_dialog = LaunchDialog(parent=None) self.launch_dialog.quit_app.connect(self.launch_dialog.close) - self.launch_dialog.quit_app.connect(lambda ec: exit(ec)) + self.launch_dialog.quit_app.connect(lambda x: sys.exit(x)) self.launch_dialog.start_app.connect(self.start_app) self.launch_dialog.start_app.connect(self.launch_dialog.close) self.launch_dialog.login() + def poke_timer(self): dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]) dt_now = datetime.utcnow() td = abs(dt_exp - dt_now) - self.timer.timeout.connect(self.re_login) self.timer.start(int(td.total_seconds() - 60) * 1000) def re_login(self): @@ -86,36 +86,18 @@ class App(RareApp): except requests.exceptions.ConnectionError: self.timer.start(3000) # try again if no connection return - dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]) - dt_now = datetime.utcnow() - td = abs(dt_exp - dt_now) - self.timer.start(int(td.total_seconds() - 60) * 1000) + self.poke_timer() def start_app(self): - for igame in self.core.get_installed_list(): - if not os.path.exists(igame.install_path): - # lk; since install_path is lost anyway, set keep_files to True - # lk: to avoid spamming the log with "file not found" errors - legendary_utils.uninstall_game(self.core, igame.app_name, keep_files=True) - logger.info(f"Uninstalled {igame.title}, because no game files exist") - continue - # lk: games that don't have an override and can't find their executable due to case sensitivity - # lk: will still erroneously require verification. This might need to be removed completely - # lk: or be decoupled from the verification requirement - if override_exe := self.core.lgd.config.get(igame.app_name, "override_exe", fallback=""): - igame_executable = override_exe - else: - igame_executable = igame.executable - if not os.path.exists(os.path.join(igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))): - igame.needs_verification = True - self.core.lgd.set_installed_game(igame.app_name, igame) - logger.info(f"{igame.title} needs verification") + self.timer = QTimer() + self.timer.timeout.connect(self.re_login) + self.poke_timer() - self.mainwindow = MainWindow() - self.mainwindow.exit_app.connect(self.exit_app) + self.main_window = MainWindow() + self.main_window.exit_app.connect(self.exit_app) if not self.args.silent: - self.mainwindow.show() + self.main_window.show() if self.args.test_start: self.exit_app(0) @@ -127,9 +109,9 @@ class App(RareApp): self.timer.stop() self.timer.deleteLater() self.timer = None - if self.mainwindow is not None: - self.mainwindow.close() - self.mainwindow = None + if self.main_window is not None: + self.main_window.close() + self.main_window = None self.rare_core.deleteLater() del self.rare_core self.processEvents() diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index 59e1a5d8..74b3f513 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -1,22 +1,26 @@ +import os import platform from logging import getLogger -from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings +from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings, pyqtSlot from PyQt5.QtWidgets import QDialog, QApplication +from legendary.models.game import Game from requests.exceptions import ConnectionError, HTTPError from rare.components.dialogs.login import LoginDialog from rare.models.apiresults import ApiResults from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton, ImageManagerSingleton from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog +from rare.utils import legendary_utils from rare.utils.misc import CloudWorker +from rare.widgets.elide_label import ElideLabel logger = getLogger("LaunchDialog") class LaunchWorker(QRunnable): class Signals(QObject): - progress = pyqtSignal(int) + progress = pyqtSignal(int, str) result = pyqtSignal(object, str) finished = pyqtSignal() @@ -26,16 +30,41 @@ class LaunchWorker(QRunnable): self.signals = LaunchWorker.Signals() self.core = LegendaryCoreSingleton() - def run(self): + def run_real(self): pass + def run(self): + self.run_real() + self.signals.deleteLater() + class ImageWorker(LaunchWorker): + # FIXME: this is a middle-ground solution for concurrent downloads + class DownloadSlot(QObject): + def __init__(self, signals: LaunchWorker.Signals): + super(ImageWorker.DownloadSlot, self).__init__() + self.signals = signals + self.counter = 0 + self.length = 0 + + @pyqtSlot(object) + def counter_inc(self, game: Game): + self.signals.progress.emit( + int(self.counter / self.length * 75), + self.tr("Downloading image for {}").format(game.app_title) + ) + self.counter += 1 + def __init__(self): super(ImageWorker, self).__init__() + # FIXME: this is a middle-ground solution for concurrent downloads + self.dl_slot = ImageWorker.DownloadSlot(self.signals) self.image_manager = ImageManagerSingleton() - def run(self): + def tr(self, t) -> str: + return QApplication.instance().translate(self.__class__.__name__, t) + + def run_real(self): # Download Images games, dlcs = self.core.get_game_and_dlc_list(update_assets=True, skip_ue=False) self.signals.result.emit((games, dlcs), "gamelist") @@ -47,12 +76,46 @@ class ImageWorker(LaunchWorker): game_list = games + dlc_list + na_games + na_dlc_list + self.dl_slot.length = len(game_list) for i, game in enumerate(game_list): if game.app_title == "Unreal Engine": game.app_title += f" {game.app_name.split('_')[-1]}" self.core.lgd.set_game_meta(game.app_name, game) - self.image_manager.download_image_blocking(game) - self.signals.progress.emit(int(i / len(game_list) * 100)) + # self.image_manager.download_image_blocking(game) + self.image_manager.download_image(game, self.dl_slot.counter_inc, priority=0) + # FIXME: this is a middle-ground solution for concurrent downloads + while self.dl_slot.counter < len(game_list): + QApplication.instance().processEvents() + self.dl_slot.deleteLater() + + igame_list = self.core.get_installed_list(include_dlc=True) + + # FIXME: incorporate installed game status checking here for now, still slow + for i, igame in enumerate(igame_list): + self.signals.progress.emit( + int(i / len(igame_list) * 25) + 75, + self.tr("Validating install for {}").format(igame.title) + ) + if not os.path.exists(igame.install_path): + # lk; since install_path is lost anyway, set keep_files to True + # lk: to avoid spamming the log with "file not found" errors + legendary_utils.uninstall_game(self.core, igame.app_name, keep_files=True) + logger.info(f"Uninstalled {igame.title}, because no game files exist") + continue + # lk: games that don't have an override and can't find their executable due to case sensitivity + # lk: will still erroneously require verification. This might need to be removed completely + # lk: or be decoupled from the verification requirement + if override_exe := self.core.lgd.config.get(igame.app_name, "override_exe", fallback=""): + igame_executable = override_exe + else: + igame_executable = igame.executable + if not os.path.exists(os.path.join(igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))): + igame.needs_verification = True + self.core.lgd.set_installed_game(igame.app_name, igame) + logger.info(f"{igame.title} needs verification") + # FIXME: end + + self.signals.progress.emit(100, self.tr("Launching Rare")) self.signals.finished.emit() @@ -61,7 +124,7 @@ class ApiRequestWorker(LaunchWorker): super(ApiRequestWorker, self).__init__() self.settings = QSettings() - def run(self) -> None: + def run_real(self) -> None: if self.settings.value("mac_meta", platform.system() == "Darwin", bool): try: result = self.core.get_game_and_dlc_list(update_assets=False, platform="Mac") @@ -102,12 +165,15 @@ class LaunchDialog(QDialog): self.ui = Ui_LaunchDialog() self.ui.setupUi(self) + self.progress_info = ElideLabel(parent=self) + self.layout().addWidget(self.progress_info) + self.core = LegendaryCoreSingleton() self.args = ArgumentsSingleton() self.thread_pool = QThreadPool().globalInstance() self.api_results = ApiResults() - self.login_dialog = LoginDialog(core=self.core, parent=self) + self.login_dialog = LoginDialog(core=self.core, parent=parent) def login(self): do_launch = True @@ -146,12 +212,12 @@ class LaunchDialog(QDialog): self.thread_pool.start(api_worker) def launch(self): + self.progress_info.setText(self.tr("Preparing Rare")) if not self.args.offline: - self.ui.image_info.setText(self.tr("Downloading Images")) image_worker = ImageWorker() image_worker.signals.result.connect(self.handle_api_worker_result) - image_worker.signals.progress.connect(self.update_image_progbar) + image_worker.signals.progress.connect(self.update_progress) # lk: start the api requests worker after the manifests have been downloaded # lk: to avoid force updating the assets twice and causing inconsistencies image_worker.signals.finished.connect(self.start_api_requests) @@ -206,14 +272,15 @@ class LaunchDialog(QDialog): if self.api_results: self.finish() - def update_image_progbar(self, i: int): - self.ui.image_prog_bar.setValue(i) + @pyqtSlot(int, str) + def update_progress(self, i: int, m: str): + self.ui.progress_bar.setValue(i) + self.progress_info.setText(m) def finish(self): self.completed += 1 if self.completed == 2: logger.info("App starting") - self.ui.image_info.setText(self.tr("Starting...")) ApiResultsSingleton(self.api_results) self.completed += 1 self.start_app.emit() diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py index 58ffc112..50a002fb 100644 --- a/rare/components/tabs/games/game_info/game_info.py +++ b/rare/components/tabs/games/game_info/game_info.py @@ -63,8 +63,8 @@ class GameInfo(QWidget, Ui_GameInfo): self.lbl_grade.setVisible(False) self.grade.setVisible(False) else: - self.steam_worker = SteamWorker(self.core) - self.steam_worker.signals.rating_signal.connect(self.grade.setText) + self.steam_worker: SteamWorker = SteamWorker(self.core) + self.steam_worker.signals.rating.connect(self.grade.setText) self.steam_worker.setAutoDelete(False) self.game_actions_stack.setCurrentIndex(0) diff --git a/rare/components/tabs/games/game_info/move_game.py b/rare/components/tabs/games/game_info/move_game.py index e4a71d3e..caf08194 100644 --- a/rare/components/tabs/games/game_info/move_game.py +++ b/rare/components/tabs/games/game_info/move_game.py @@ -135,11 +135,15 @@ class MoveGamePopUp(QWidget): return True, dir_selected, str() def update_game(self, app_name): - igame = self.core.get_installed_game(app_name, False) + igame = self.core.get_installed_game(app_name, skip_sync=True) if igame is None: return self.install_path = igame.install_path + # FIXME: Make edit_func lighter instead of blocking signals + self.move_path_edit.line_edit.blockSignals(True) self.move_path_edit.setText(igame.install_path) + # FIXME: Make edit_func lighter instead of blocking signals + self.move_path_edit.line_edit.blockSignals(False) self.warn_overwriting.setText( self.tr("Moving here will overwrite the dir/file {}/").format(Path(self.install_path).stem) ) diff --git a/rare/components/tabs/games/game_info/uninstalled_info.py b/rare/components/tabs/games/game_info/uninstalled_info.py index 85f8b188..08a12c65 100644 --- a/rare/components/tabs/games/game_info/uninstalled_info.py +++ b/rare/components/tabs/games/game_info/uninstalled_info.py @@ -87,7 +87,7 @@ class UninstalledInfo(QWidget, Ui_GameInfo): self.install_button.clicked.connect(self.install_game) if platform.system() != "Windows": self.steam_worker = SteamWorker(self.core) - self.steam_worker.signals.rating_signal.connect(self.grade.setText) + self.steam_worker.signals.rating.connect(self.grade.setText) self.steam_worker.setAutoDelete(False) else: diff --git a/rare/shared/image_manager.py b/rare/shared/image_manager.py index 1412a96e..7e3054b8 100644 --- a/rare/shared/image_manager.py +++ b/rare/shared/image_manager.py @@ -1,9 +1,8 @@ -from __future__ import annotations - import hashlib import json import pickle import zlib +# from concurrent import futures from logging import getLogger from pathlib import Path from typing import TYPE_CHECKING @@ -30,6 +29,8 @@ from rare.lgndr.core import LegendaryCore from rare.models.signals import GlobalSignals from rare.utils.paths import image_dir, resources_path +# from requests_futures.sessions import FuturesSession + if TYPE_CHECKING: pass @@ -86,8 +87,8 @@ class ImageSize: class ImageManager(QObject): class Worker(QRunnable): class Signals(QObject): - # str: app_name - completed = pyqtSignal(str) + # object: Game + completed = pyqtSignal(object) def __init__(self, func: Callable, updates: List, json_data: Dict, game: Game): super(ImageManager.Worker, self).__init__() @@ -101,7 +102,7 @@ class ImageManager(QObject): def run(self): self.func(self.updates, self.json_data, self.game) logger.debug(f" Emitting singal for game {self.game.app_name} - {self.game.app_title}") - self.signals.completed.emit(self.game.app_name) + self.signals.completed.emit(self.game) def __init__(self, signals: GlobalSignals, core: LegendaryCore): # lk: the ordering in __img_types matters for the order of fallbacks @@ -179,7 +180,7 @@ class ImageManager(QObject): return updates, json_data - def __download(self, updates, json_data, game) -> bool: + def __download(self, updates, json_data, game, use_async: bool = False) -> bool: # Decompress existing image.cache if not self.__img_cache(game.app_name).is_file(): cache_data = dict(zip(self.__img_types, [None] * len(self.__img_types))) @@ -193,6 +194,22 @@ class ImageManager(QObject): if cache_data[image["type"]] is None or json_data[image["type"]] != image["md5"] ] + # Download + # # lk: Keep this so I don't have to go looking for it again, + # # lk: it might be useful in the future. + # if use_async and len(updates) > 1: + # session = FuturesSession(max_workers=len(self.__img_types)) + # image_requests = [] + # for image in updates: + # logger.info(f"Downloading {image['type']} for {game.app_title}") + # json_data[image["type"]] = image["md5"] + # payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()} + # req = session.get(image["url"], params=payload) + # req.image_type = image["type"] + # image_requests.append(req) + # for req in futures.as_completed(image_requests): + # cache_data[req.image_type] = req.result().content + # else: for image in updates: logger.info(f"Downloading {image['type']} for {game.app_title}") json_data[image["type"]] = image["md5"] @@ -291,18 +308,18 @@ class ImageManager(QObject): return data def download_image( - self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False + self, game: Game, load_callback: Callable[[Game], None], priority: int, force: bool = False ) -> None: updates, json_data = self.__prepare_download(game, force) if not updates: - load_callback() + load_callback(game) return if updates and game.app_name not in self.__worker_app_names: image_worker = ImageManager.Worker(self.__download, updates, json_data, game) self.__worker_app_names.append(game.app_name) image_worker.signals.completed.connect(load_callback) - image_worker.signals.completed.connect(lambda app_name: self.__worker_app_names.remove(app_name)) + image_worker.signals.completed.connect(lambda g: self.__worker_app_names.remove(g.app_name)) self.threadpool.start(image_worker, priority) def download_image_blocking(self, game: Game, force: bool = False) -> None: @@ -310,7 +327,7 @@ class ImageManager(QObject): if not updates: return if updates: - self.__download(updates, json_data, game) + self.__download(updates, json_data, game, use_async=True) def __get_cover( self, container: Union[Type[QPixmap], Type[QImage]], app_name: str, color: bool = True diff --git a/rare/ui/components/dialogs/launch_dialog.py b/rare/ui/components/dialogs/launch_dialog.py index 5e9d70cd..39b4e3f8 100644 --- a/rare/ui/components/dialogs/launch_dialog.py +++ b/rare/ui/components/dialogs/launch_dialog.py @@ -1,32 +1,36 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'launch_dialog.ui' +# Form implementation generated from reading ui file 'rare/ui/components/dialogs/launch_dialog.ui' # -# Created by: PyQt5 UI code generator 5.15.4 +# Created by: PyQt5 UI code generator 5.15.7 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets class Ui_LaunchDialog(object): def setupUi(self, LaunchDialog): LaunchDialog.setObjectName("LaunchDialog") - LaunchDialog.resize(400, 168) - self.verticalLayout = QtWidgets.QVBoxLayout(LaunchDialog) - self.verticalLayout.setObjectName("verticalLayout") + LaunchDialog.resize(400, 160) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(LaunchDialog.sizePolicy().hasHeightForWidth()) + LaunchDialog.setSizePolicy(sizePolicy) + LaunchDialog.setMinimumSize(QtCore.QSize(400, 160)) + LaunchDialog.setMaximumSize(QtCore.QSize(400, 160)) + self.launch_dialog_layout = QtWidgets.QVBoxLayout(LaunchDialog) + self.launch_dialog_layout.setObjectName("launch_dialog_layout") self.title_label = QtWidgets.QLabel(LaunchDialog) self.title_label.setObjectName("title_label") - self.verticalLayout.addWidget(self.title_label) - self.image_prog_bar = QtWidgets.QProgressBar(LaunchDialog) - self.image_prog_bar.setProperty("value", 0) - self.image_prog_bar.setObjectName("image_prog_bar") - self.verticalLayout.addWidget(self.image_prog_bar) - self.image_info = QtWidgets.QLabel(LaunchDialog) - self.image_info.setObjectName("image_info") - self.verticalLayout.addWidget(self.image_info) + self.launch_dialog_layout.addWidget(self.title_label) + self.progress_bar = QtWidgets.QProgressBar(LaunchDialog) + self.progress_bar.setProperty("value", 0) + self.progress_bar.setObjectName("progress_bar") + self.launch_dialog_layout.addWidget(self.progress_bar) self.retranslateUi(LaunchDialog) QtCore.QMetaObject.connectSlotsByName(LaunchDialog) @@ -35,7 +39,6 @@ class Ui_LaunchDialog(object): _translate = QtCore.QCoreApplication.translate LaunchDialog.setWindowTitle(_translate("LaunchDialog", "Launching Rare")) self.title_label.setText(_translate("LaunchDialog", "

Launching Rare

")) - self.image_info.setText(_translate("LaunchDialog", "Downloading images")) if __name__ == "__main__": diff --git a/rare/ui/components/dialogs/launch_dialog.ui b/rare/ui/components/dialogs/launch_dialog.ui index 0916d8ce..7f5d873e 100644 --- a/rare/ui/components/dialogs/launch_dialog.ui +++ b/rare/ui/components/dialogs/launch_dialog.ui @@ -7,13 +7,31 @@ 0 0 400 - 168 + 160 + + + 0 + 0 + + + + + 400 + 160 + + + + + 400 + 160 + + Launching Rare - + @@ -22,19 +40,12 @@ - + 0 - - - - Downloading images - - - diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index 172e233e..57ba3e5f 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -16,7 +16,7 @@ url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/" class SteamWorker(QRunnable): class Signals(QObject): - rating_signal = pyqtSignal(str) + rating = pyqtSignal(str) app_name: str = "" @@ -39,7 +39,7 @@ class SteamWorker(QRunnable): self.app_name = app_name def run(self) -> None: - self.signals.rating_signal.emit( + self.signals.rating.emit( self.ratings.get(get_rating(self.app_name), self.ratings["fail"]) ) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..1555712b --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +build +wheel +black +toml +nuitka +ordered-set +PyQt5-stubs diff --git a/setup.py b/setup.py index 21f9bfa3..69c489a8 100644 --- a/setup.py +++ b/setup.py @@ -47,5 +47,5 @@ setuptools.setup( python_requires=">=3.8", entry_points=dict(console_scripts=["rare=rare.__main__:main"]), install_requires=requirements, - extras_require=optional_reqs + extras_require=optional_reqs, )