mirror of
https://github.com/bdnugget/runekit.git
synced 2026-11-07 09:07:30 +00:00
Compare commits
29
Commits
No files matched your search
@@ -0,0 +1,67 @@
|
||||
name: build-appimage
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Get linuxdeploy
|
||||
run: |
|
||||
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -O linuxdeploy.AppImage
|
||||
chmod +x linuxdeploy.AppImage
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install poetry
|
||||
poetry install
|
||||
- name: Build resources
|
||||
run: poetry run make runekit/_resources.py
|
||||
- name: Build AppImage
|
||||
run: LINUXDEPLOY=./linuxdeploy.AppImage make dist/RuneKit.AppImage
|
||||
|
||||
# Deploy
|
||||
- name: Bump tag
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: richardsimko/update-tag@v1
|
||||
with:
|
||||
tag_name: continuous
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: dev-drprasad/delete-tag-and-release@v0.1.3
|
||||
if: github.ref == 'refs/heads/master'
|
||||
with:
|
||||
delete_release: true
|
||||
tag_name: continuous
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload to CI
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.ref == 'refs/heads/master'
|
||||
with:
|
||||
tag_name: continuous
|
||||
prerelease: true
|
||||
name: Continuous Integration
|
||||
body: These files are automatically built with GitHub Actions from the master branch.
|
||||
files: |
|
||||
dist/RuneKit.AppImage
|
||||
dist/runekit.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload to CI
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
dist/RuneKit.AppImage
|
||||
dist/runekit.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -7,3 +7,4 @@ setup.py
|
||||
__pycache__
|
||||
runekit/_resources.py
|
||||
RuneKitApp.spec
|
||||
*.AppImage
|
||||
@@ -1,17 +1,44 @@
|
||||
dev: runekit/_resources.py
|
||||
LINUXDEPLOY ?= linuxdeploy-$(shell uname -m).AppImage
|
||||
|
||||
dist/runekit.tar.gz: main.py poetry.lock $(wildcard runekit/**/*)
|
||||
poetry build -f sdist
|
||||
cd dist; cp runekit-*.tar.gz runekit.tar.gz
|
||||
dev: runekit/_resources.py
|
||||
|
||||
runekit/_resources.py: resources.qrc $(wildcard runekit/**/*.js) $(wildcard runekit/**/*.png)
|
||||
pyside2-rcc $< -o $@
|
||||
|
||||
dist/RuneKitApp.app: main.py poetry.lock $(wildcard runekit/**/*)
|
||||
# Sdist
|
||||
|
||||
dist/runekit.tar.gz: main.py poetry.lock runekit/_resources.py $(wildcard runekit/**/*)
|
||||
poetry build -f sdist
|
||||
cd dist; cp runekit-*.tar.gz runekit.tar.gz
|
||||
|
||||
# Mac
|
||||
|
||||
dist/RuneKitApp.app: main.py poetry.lock runekit/_resources.py $(wildcard runekit/**/*)
|
||||
pyinstaller -w -n RuneKitApp \
|
||||
--exclude-module tkinter \
|
||||
-s -d noarchive \
|
||||
--osx-bundle-identifier de.cupco.runekit \
|
||||
$<
|
||||
|
||||
# AppImage
|
||||
|
||||
build/python3.9.1.AppImage:
|
||||
mkdir build || true
|
||||
wget https://github.com/niess/python-appimage/releases/download/python3.9/python3.9.1-cp39-cp39-manylinux1_x86_64.AppImage -O "$@"
|
||||
chmod +x "$@"
|
||||
|
||||
build/appdir: build/python3.9.1.AppImage
|
||||
$< --appimage-extract
|
||||
mv squashfs-root build/appdir
|
||||
|
||||
dist/RuneKit.AppImage: dist/runekit.tar.gz build/appdir deploy/runekit-appimage.sh
|
||||
build/appdir/usr/bin/python3 -m pip install dist/runekit.tar.gz
|
||||
rm $(wildcard build/appdir/*.desktop) $(wildcard build/appdir/usr/share/applications/*.desktop) $(wildcard build/appdir/usr/share/metainfo/*)
|
||||
cp deploy/RuneKit.desktop build/appdir/
|
||||
cp deploy/RuneKit.desktop build/appdir/usr/share/applications/
|
||||
cp deploy/RuneKit.appdata.xml build/appdir/usr/share/metainfo/
|
||||
cp deploy/runekit-appimage.sh build/appdir/AppRun
|
||||
$(LINUXDEPLOY) --appdir build/appdir --output appimage
|
||||
cp RuneKit-*.AppImage "$@"
|
||||
|
||||
.PHONY: dev
|
||||
@@ -7,14 +7,39 @@ Alt1-compatible toolbox for RuneScape 3, for Linux and macOS.
|
||||
* [Compatibility](https://github.com/whs/runekit/wiki/App-Compatibility)
|
||||
* [macOS installation guide](docs/macos-setup.md)
|
||||
|
||||
## Running
|
||||
## Installing
|
||||
|
||||
### Linux
|
||||
|
||||
1. [Download RuneKit.AppImage](https://github.com/whs/runekit/releases/tag/continuous)
|
||||
2. Mark file as executable (`chmod +x`)
|
||||
3. Start the game
|
||||
4. Run `RuneKit.AppImage`.
|
||||
- On first start it will download app list
|
||||
5. Right click the tray icon and start any application
|
||||
|
||||
### macOS
|
||||
|
||||
See developer guide in the next section.
|
||||
|
||||
You will need to add Python in System Preferences > Security > Privacy in these sections:
|
||||
|
||||
- Accessibility
|
||||
- Screen Recording
|
||||
|
||||
Note that Python might appear as the closest macOS application (eg. your terminal emulator) instead of Python
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
[See wiki](https://github.com/whs/runekit/wiki/Troubleshooting)
|
||||
|
||||
## Developer
|
||||
|
||||
This project use [Poetry](https://python-poetry.org) as package manager.
|
||||
|
||||
Requires Poetry 1.1.
|
||||
|
||||
```sh
|
||||
# Try this first
|
||||
poetry install
|
||||
# If previous fails and you're on Big Sur, try this instead
|
||||
SYSTEM_VERSION_COMPAT=1 poetry install
|
||||
@@ -27,21 +52,6 @@ poetry run python main.py https://runeapps.org/apps/alt1/afkscape/appconfig.json
|
||||
poetry run python main.py
|
||||
```
|
||||
|
||||
### Linux additional instruction
|
||||
|
||||
Requires libxcb to be installed
|
||||
|
||||
### macOS additional instruction
|
||||
|
||||
You will need to add Python in System Preferences > Security > Privacy in these sections:
|
||||
|
||||
- Accessibility
|
||||
- Screen Recording
|
||||
|
||||
Note that Python might appear as the closest macOS application (eg. your terminal emulator) instead of Python
|
||||
|
||||
## Developer
|
||||
|
||||
Start with `--remote-debugging-port=9222` to enable remote debugger protocol.
|
||||
To debug, go to `chrome://inspect` on Chrome/Chromium.
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from Cython.Build import cythonize
|
||||
|
||||
# use cythonize to build the extensions
|
||||
modules = [
|
||||
# "runekit/image/format.pyx",
|
||||
]
|
||||
|
||||
extensions = cythonize(modules)
|
||||
|
||||
|
||||
def build(setup_kwargs):
|
||||
"""Needed for the poetry building interface."""
|
||||
|
||||
setup_kwargs.update({"ext_modules": extensions, "scripts": ["main.py"]})
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>runekit</id>
|
||||
<metadata_license>GPLv3</metadata_license>
|
||||
<project_license>GPLv3</project_license>
|
||||
<name>RuneKit</name>
|
||||
<description>
|
||||
<p>Alt1-compatible toolbox for RuneScape 3</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">RuneKit.desktop</launchable>
|
||||
<url type="homepage">https://github.com/whs/runekit</url>
|
||||
<provides>
|
||||
<binary>runekit</binary>
|
||||
<python3>runekit</python3>
|
||||
</provides>
|
||||
</component>
|
||||
@@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=RuneKit
|
||||
Exec=runekit
|
||||
Comment=Alt1-compatible toolbox for RuneScape 3
|
||||
Icon=python
|
||||
Categories=Game;
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Export APPRUN if running from an extracted image
|
||||
self="$(readlink -f -- $0)"
|
||||
here="${self%/*}"
|
||||
APPDIR="${APPDIR:-${here}}"
|
||||
|
||||
# Export SSL certificate
|
||||
export SSL_CERT_FILE="${APPDIR}/opt/_internal/certs.pem"
|
||||
export PYTHONHOME="${APPDIR}/opt/python3.9"
|
||||
|
||||
exec $APPDIR/usr/bin/runekit $@
|
||||
File renamed without changes.
@@ -1,82 +1,4 @@
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import click
|
||||
from PySide2.QtCore import QSettings, Qt, QTimer
|
||||
from PySide2.QtWidgets import (
|
||||
QApplication,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
import runekit._resources
|
||||
from runekit import browser
|
||||
from runekit.game import get_platform_manager
|
||||
from runekit.host import Host
|
||||
|
||||
|
||||
@click.command(
|
||||
context_settings=dict(
|
||||
ignore_unknown_options=True,
|
||||
)
|
||||
)
|
||||
@click.option("--game-index", default=0, help="Game instance index, starting from 0")
|
||||
@click.argument("qt_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@click.argument("app_url", required=False)
|
||||
def main(app_url, game_index, qt_args):
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logging.info("Starting QtWebEngine")
|
||||
browser.init()
|
||||
|
||||
app = QApplication(["runekit", *qt_args])
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
app.setOrganizationName("cupco.de")
|
||||
app.setOrganizationDomain("cupco.de")
|
||||
app.setApplicationName("RuneKit")
|
||||
|
||||
signal.signal(signal.SIGINT, lambda no, frame: app.quit())
|
||||
|
||||
timer = QTimer()
|
||||
timer.start(300)
|
||||
timer.timeout.connect(lambda: None)
|
||||
|
||||
QSettings.setDefaultFormat(QSettings.IniFormat)
|
||||
|
||||
try:
|
||||
game_manager = get_platform_manager()
|
||||
host = Host(game_manager)
|
||||
|
||||
if app_url == "settings":
|
||||
host.open_settings()
|
||||
host.setting_dialog.setAttribute(Qt.WA_DeleteOnClose)
|
||||
host.setting_dialog.destroyed.connect(app.quit)
|
||||
elif app_url:
|
||||
logging.info("Loading app")
|
||||
game_app = host.launch_app_from_url(app_url)
|
||||
game_app.window.destroyed.connect(app.quit)
|
||||
else:
|
||||
if not host.app_store.has_default_apps():
|
||||
host.app_store.load_default_apps()
|
||||
|
||||
app.exec_()
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
msg = QMessageBox(
|
||||
QMessageBox.Critical,
|
||||
"Oh No!",
|
||||
f"Fatal error: \n\n{e.__class__.__name__}: {e}",
|
||||
)
|
||||
msg.setDetailedText(traceback.format_exc())
|
||||
msg.exec_()
|
||||
|
||||
raise
|
||||
finally:
|
||||
if game_manager is not None:
|
||||
logging.debug("Stopping game manager")
|
||||
game_manager.stop()
|
||||
|
||||
from runekit.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+38
-82
@@ -71,14 +71,6 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "cython"
|
||||
version = "0.29.21"
|
||||
description = "The Cython compiler for writing C extensions for the Python language."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "2.10"
|
||||
@@ -135,7 +127,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "8.1.0"
|
||||
version = "8.1.2"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -179,7 +171,7 @@ hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2020.11"
|
||||
version = "2021.1"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -355,7 +347,7 @@ six = "*"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9,<3.10"
|
||||
content-hash = "c48f57d1a7ec53d96388f6fc1e625f123fac3427ecfb83ef70dbe1f364b5ad53"
|
||||
content-hash = "a6e17c47f17532e7594f59aa7bcb6bfc0b06c3f27c3b50a4a7cb8b6fd829451a"
|
||||
|
||||
[metadata.files]
|
||||
altgraph = [
|
||||
@@ -420,43 +412,6 @@ click = [
|
||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||
]
|
||||
cython = [
|
||||
{file = "Cython-0.29.21-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c541b2b49c6638f2b5beb9316726db84a8d1c132bf31b942dae1f9c7f6ad3b92"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b8d8497091c1dc8705d1575c71e908a93b1f127a174b2d472020f3d84263ac28"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:695a6bcaf9e12b1e471dfce96bbecf22a1487adc2ac6106b15960a2b51b97f5d"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-win32.whl", hash = "sha256:171b9f70ceafcec5852089d0f9c1e75b0d554f46c882cd4e2e4acaba9bd7d148"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-win_amd64.whl", hash = "sha256:539e59949aab4955c143a468810123bf22d3e8556421e1ce2531ed4893914ca0"},
|
||||
{file = "Cython-0.29.21-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e93acd1f603a0c1786e0841f066ae7cef014cf4750e3cd06fd03cfdf46361419"},
|
||||
{file = "Cython-0.29.21-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:2922e3031ba9ebbe7cb9200b585cc33b71d66023d78450dcb883f824f4969371"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:497841897942f734b0abc2dead2d4009795ee992267a70a23485fd0e937edc0b"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0ac10bf476476a9f7ef61ec6e44c280ef434473124ad31d3132b720f7b0e8d2a"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-win32.whl", hash = "sha256:31c71a615f38401b0dc1f2a5a9a6c421ffd8908c4cd5bbedc4014c1b876488e8"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-win_amd64.whl", hash = "sha256:c4b78356074fcaac04ecb4de289f11d506e438859877670992ece11f9c90f37b"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:b2f9172e4d6358f33ecce6a4339b5960f9f83eab67ea244baa812737793826b7"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:856c7fb31d247ce713d60116375e1f8153d0291ab5e92cca7d8833a524ba9991"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:715294cd2246b39a8edca464a8366eb635f17213e4a6b9e74e52d8b877a8cb63"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-win32.whl", hash = "sha256:23f3a00b843a19de8bb4468b087db5b413a903213f67188729782488d67040e0"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-win_amd64.whl", hash = "sha256:ccb77faeaad99e99c6c444d04862c6cf604204fe0a07d4c8f9cbf2c9012d7d5a"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e272ed97d20b026f4f25a012b25d7d7672a60e4f72b9ca385239d693cd91b2d5"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:8c6e25e9cc4961bb2abb1777c6fa9d0fa2d9b014beb3276cebe69996ff162b78"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:57ead89128dee9609119c93d3926c7a2add451453063147900408a50144598c6"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-win32.whl", hash = "sha256:0e25c209c75df8785480dcef85db3d36c165dbc0f4c503168e8763eb735704f2"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-win_amd64.whl", hash = "sha256:a0674f246ad5e1571ef29d4c5ec1d6ecabe9e6c424ad0d6fee46b914d5d24d69"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5da187bebe38030325e1c0b5b8a804d489410be2d384c0ef3ba39493c67eb51e"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9ce5e5209f8406ffc2b058b1293cce7a954911bb7991e623564d489197c9ba30"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5e545a48f919e40079b0efe7b0e081c74b96f9ef25b9c1ff4cdbd95764426b58"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-win32.whl", hash = "sha256:c8435959321cf8aec867bbad54b83b7fb8343204b530d85d9ea7a1f5329d5ac2"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-win_amd64.whl", hash = "sha256:540b3bee0711aac2e99bda4fa0a46dbcd8c74941666bfc1ef9236b1a64eeffd9"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93f5fed1c9445fb7afe20450cdaf94b0e0356d47cc75008105be89c6a2e417b1"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9207fdedc7e789a3dcaca628176b80c82fbed9ae0997210738cbb12536a56699"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:603b9f1b8e93e8b494d3e89320c410679e21018e48b6cbc77280f5db71f17dc0"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-win32.whl", hash = "sha256:473df5d5e400444a36ed81c6596f56a5b52a3481312d0a48d68b777790f730ae"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8a8a31b9e8860634adbca30fea1d0c7f08e208b3d7611f3e580e5f20992e5d7"},
|
||||
{file = "Cython-0.29.21-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7ebaa8800c376bcdae596fb1372cb4232a5ef957619d35839520d2786f2debb9"},
|
||||
{file = "Cython-0.29.21-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c111ac9abdf715762e4fb87395e59d61c0fbb6ce79eb2e24167700b6cfa8ba79"},
|
||||
{file = "Cython-0.29.21-py2.py3-none-any.whl", hash = "sha256:5c4276fdcbccdf1e3c1756c7aeb8395e9a36874fa4d30860e7694f43d325ae13"},
|
||||
{file = "Cython-0.29.21.tar.gz", hash = "sha256:e57acb89bd55943c8d8bf813763d20b9099cc7165c0f16b707631a7654be9cad"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
||||
@@ -527,38 +482,39 @@ pathspec = [
|
||||
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-win32.whl", hash = "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-win32.whl", hash = "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-win32.whl", hash = "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-win32.whl", hash = "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865"},
|
||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9"},
|
||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913"},
|
||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5"},
|
||||
{file = "Pillow-8.1.0.tar.gz", hash = "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba"},
|
||||
{file = "Pillow-8.1.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:5cf03b9534aca63b192856aa601c68d0764810857786ea5da652581f3a44c2b0"},
|
||||
{file = "Pillow-8.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f91b50ad88048d795c0ad004abbe1390aa1882073b1dca10bfd55d0b8cf18ec5"},
|
||||
{file = "Pillow-8.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5762ebb4436f46b566fc6351d67a9b5386b5e5de4e58fdaa18a1c83e0e20f1a8"},
|
||||
{file = "Pillow-8.1.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e2cd8ac157c1e5ae88b6dd790648ee5d2777e76f1e5c7d184eaddb2938594f34"},
|
||||
{file = "Pillow-8.1.2-cp36-cp36m-win32.whl", hash = "sha256:72027ebf682abc9bafd93b43edc44279f641e8996fb2945104471419113cfc71"},
|
||||
{file = "Pillow-8.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d1d6bca39bb6dd94fba23cdb3eeaea5e30c7717c5343004d900e2a63b132c341"},
|
||||
{file = "Pillow-8.1.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:90882c6f084ef68b71bba190209a734bf90abb82ab5e8f64444c71d5974008c6"},
|
||||
{file = "Pillow-8.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:89e4c757a91b8c55d97c91fa09c69b3677c227b942fa749e9a66eef602f59c28"},
|
||||
{file = "Pillow-8.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8c4e32218c764bc27fe49b7328195579581aa419920edcc321c4cb877c65258d"},
|
||||
{file = "Pillow-8.1.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a01da2c266d9868c4f91a9c6faf47a251f23b9a862dce81d2ff583135206f5be"},
|
||||
{file = "Pillow-8.1.2-cp37-cp37m-win32.whl", hash = "sha256:30d33a1a6400132e6f521640dd3f64578ac9bfb79a619416d7e8802b4ce1dd55"},
|
||||
{file = "Pillow-8.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:71b01ee69e7df527439d7752a2ce8fb89e19a32df484a308eca3e81f673d3a03"},
|
||||
{file = "Pillow-8.1.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:5a2d957eb4aba9d48170b8fe6538ec1fbc2119ffe6373782c03d8acad3323f2e"},
|
||||
{file = "Pillow-8.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:87f42c976f91ca2fc21a3293e25bd3cd895918597db1b95b93cbd949f7d019ce"},
|
||||
{file = "Pillow-8.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:15306d71a1e96d7e271fd2a0737038b5a92ca2978d2e38b6ced7966583e3d5af"},
|
||||
{file = "Pillow-8.1.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:71f31ee4df3d5e0b366dd362007740106d3210fb6a56ec4b581a5324ba254f06"},
|
||||
{file = "Pillow-8.1.2-cp38-cp38-win32.whl", hash = "sha256:98afcac3205d31ab6a10c5006b0cf040d0026a68ec051edd3517b776c1d78b09"},
|
||||
{file = "Pillow-8.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:328240f7dddf77783e72d5ed79899a6b48bc6681f8d1f6001f55933cb4905060"},
|
||||
{file = "Pillow-8.1.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bead24c0ae3f1f6afcb915a057943ccf65fc755d11a1410a909c1fefb6c06ad1"},
|
||||
{file = "Pillow-8.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81b3716cc9744ffdf76b39afb6247eae754186838cedad0b0ac63b2571253fe6"},
|
||||
{file = "Pillow-8.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:63cd413ac52ee3f67057223d363f4f82ce966e64906aea046daf46695e3c8238"},
|
||||
{file = "Pillow-8.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8565355a29655b28fdc2c666fd9a3890fe5edc6639d128814fafecfae2d70910"},
|
||||
{file = "Pillow-8.1.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1940fc4d361f9cc7e558d6f56ff38d7351b53052fd7911f4b60cd7bc091ea3b1"},
|
||||
{file = "Pillow-8.1.2-cp39-cp39-win32.whl", hash = "sha256:46c2bcf8e1e75d154e78417b3e3c64e96def738c2a25435e74909e127a8cba5e"},
|
||||
{file = "Pillow-8.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:aeab4cd016e11e7aa5cfc49dcff8e51561fa64818a0be86efa82c7038e9369d0"},
|
||||
{file = "Pillow-8.1.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:74cd9aa648ed6dd25e572453eb09b08817a1e3d9f8d1bd4d8403d99e42ea790b"},
|
||||
{file = "Pillow-8.1.2-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:e5739ae63636a52b706a0facec77b2b58e485637e1638202556156e424a02dc2"},
|
||||
{file = "Pillow-8.1.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:903293320efe2466c1ab3509a33d6b866dc850cfd0c5d9cc92632014cec185fb"},
|
||||
{file = "Pillow-8.1.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:5daba2b40782c1c5157a788ec4454067c6616f5a0c1b70e26ac326a880c2d328"},
|
||||
{file = "Pillow-8.1.2-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:1f93f2fe211f1ef75e6f589327f4d4f8545d5c8e826231b042b483d8383e8a7c"},
|
||||
{file = "Pillow-8.1.2-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:6efac40344d8f668b6c4533ae02a48d52fd852ef0654cc6f19f6ac146399c733"},
|
||||
{file = "Pillow-8.1.2-pp37-pypy37_pp73-win32.whl", hash = "sha256:f36c3ff63d6fc509ce599a2f5b0d0732189eed653420e7294c039d342c6e204a"},
|
||||
{file = "Pillow-8.1.2.tar.gz", hash = "sha256:b07c660e014852d98a00a91adfbe25033898a9d90a8f39beb2437d22a203fc44"},
|
||||
]
|
||||
psutil = [
|
||||
{file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
|
||||
@@ -598,8 +554,8 @@ pyinstaller = [
|
||||
{file = "pyinstaller-4.2.tar.gz", hash = "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c"},
|
||||
]
|
||||
pyinstaller-hooks-contrib = [
|
||||
{file = "pyinstaller-hooks-contrib-2020.11.tar.gz", hash = "sha256:fc3290a2ca337d1d58c579c223201360bfe74caed6454eaf5a2550b77dbda45c"},
|
||||
{file = "pyinstaller_hooks_contrib-2020.11-py2.py3-none-any.whl", hash = "sha256:fa8280b79d8a2b267a2e43ff44f73b3e4a68fc8d205b8d34e8e06c960f7c2fcf"},
|
||||
{file = "pyinstaller-hooks-contrib-2021.1.tar.gz", hash = "sha256:892310e6363655838485ee748bf1c5e5cade7963686d9af8650ee218a3e0b031"},
|
||||
{file = "pyinstaller_hooks_contrib-2021.1-py2.py3-none-any.whl", hash = "sha256:27558072021857d89524c42136feaa2ffe4f003f1bdf0278f9b24f6902c1759c"},
|
||||
]
|
||||
pyobjc-core = [
|
||||
{file = "pyobjc-core-7.1.tar.gz", hash = "sha256:a0616d5d816b4471f8f782c3a9a8923d2cc85014d88ad4f7fec694be9e6ea349"},
|
||||
|
||||
+3
-7
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
license = "GPL"
|
||||
include = ["runekit/_resources.py"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9,<3.10"
|
||||
@@ -20,13 +21,8 @@ psutil = "^5.8.0"
|
||||
opencv-python-headless = "^4.5.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
Cython = "^0.29.21"
|
||||
black = "^20.8b1"
|
||||
pyinstaller = [{ version="^4.2", platform="darwin" }]
|
||||
|
||||
[tool.poetry.build]
|
||||
script = "build.py"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.1.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
[tool.poetry.scripts]
|
||||
runekit = 'runekit.main:main'
|
||||
@@ -27,6 +27,20 @@ class App:
|
||||
self.game_instance = game_instance
|
||||
self.source_url = source_url
|
||||
|
||||
def close(self):
|
||||
if self.window:
|
||||
self.window.deleteLater()
|
||||
self.window = None
|
||||
if self.alt1api:
|
||||
self.alt1api.deleteLater()
|
||||
self.alt1api = None
|
||||
if self.web_profile:
|
||||
self.web_profile.deleteLater()
|
||||
self.web_profile = None
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def get_window(self, **kwargs) -> AppWindow:
|
||||
self.window = AppWindow(app=self, **kwargs)
|
||||
self.window.winId() # force native window
|
||||
|
||||
+88
-30
@@ -1,8 +1,10 @@
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
from typing import Iterator, Tuple, Optional, Union
|
||||
from typing import Iterator, Tuple, Optional, Union, List
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
@@ -15,7 +17,7 @@ from PySide2.QtCore import (
|
||||
QStandardPaths,
|
||||
)
|
||||
from PySide2.QtGui import QIcon, QPixmap
|
||||
from PySide2.QtWidgets import QProgressDialog
|
||||
from PySide2.QtWidgets import QProgressDialog, QMessageBox
|
||||
|
||||
from runekit.alt1.schema import AppManifest
|
||||
from runekit.alt1.utils import fetch_bom_json
|
||||
@@ -34,23 +36,49 @@ class _FetchRegistryThread(QThread):
|
||||
progress = Signal(int)
|
||||
items = Signal(int)
|
||||
label = Signal(str)
|
||||
failed = Signal(tuple)
|
||||
|
||||
canceled = False
|
||||
|
||||
def __init__(self, parent: "AppStore"):
|
||||
def __init__(
|
||||
self,
|
||||
parent: "AppStore",
|
||||
apps_manifest: Optional[str] = None,
|
||||
apps: Optional[List[str]] = None,
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
assert (not apps_manifest and apps) or (
|
||||
not apps and apps_manifest
|
||||
), "One of apps_manifest or apps must be set but not both. apps_manifest={}, apps={}".format(
|
||||
apps_manifest, apps
|
||||
)
|
||||
|
||||
self.appstore = parent
|
||||
self.apps_manifest = apps_manifest
|
||||
self.apps = apps
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
default_apps = fetch_bom_json(REGISTRY_URL)
|
||||
self.items.emit(len(default_apps) + 1)
|
||||
self.progress.emit(1)
|
||||
for index, app in enumerate(default_apps):
|
||||
self.label.emit(f"Installing {app['name']}")
|
||||
url = urljoin(REGISTRY_URL, app["url"])
|
||||
self.add_app(url, app["folder"])
|
||||
self.progress.emit(index + 2)
|
||||
progress_extra = 0
|
||||
if self.apps_manifest:
|
||||
apps = fetch_bom_json(REGISTRY_URL)
|
||||
self.items.emit(len(apps) + 1)
|
||||
self.progress.emit(1)
|
||||
progress_extra = 1
|
||||
else:
|
||||
apps = self.apps
|
||||
self.items.emit(len(apps))
|
||||
|
||||
for index, app in enumerate(apps):
|
||||
if type(app) == str:
|
||||
self.label.emit(f"Installing app {app}")
|
||||
self.add_app(app)
|
||||
else:
|
||||
self.label.emit(f"Installing {app['name']}")
|
||||
url = urljoin(REGISTRY_URL, app["url"])
|
||||
self.add_app(url, app["folder"])
|
||||
|
||||
self.progress.emit(index + progress_extra + 1)
|
||||
|
||||
if self.canceled:
|
||||
return
|
||||
@@ -59,12 +87,13 @@ class _FetchRegistryThread(QThread):
|
||||
self.progress.emit(100)
|
||||
|
||||
def add_app(self, url, folder=""):
|
||||
manifest = fetch_bom_json(url)
|
||||
try:
|
||||
manifest = fetch_bom_json(url)
|
||||
self.appstore.add_app(url, manifest)
|
||||
self.appstore.add_app_to_folder(app_id(url), folder)
|
||||
except AddAppError:
|
||||
except:
|
||||
logger.warning("Unable to add application %s", url, exc_info=True)
|
||||
self.failed.emit(sys.exc_info())
|
||||
|
||||
@Slot()
|
||||
def cancel(self):
|
||||
@@ -81,7 +110,7 @@ class AppStore(QObject):
|
||||
qt_write_base = Path(
|
||||
QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)
|
||||
)
|
||||
qt_write_base.mkdir(exist_ok=True)
|
||||
qt_write_base.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.icon_write_dir = qt_write_base / "app_icons"
|
||||
self.icon_write_dir.mkdir(exist_ok=True)
|
||||
@@ -91,28 +120,57 @@ class AppStore(QObject):
|
||||
|
||||
def load_default_apps(self):
|
||||
def on_progress(value):
|
||||
if value != self.default_app_progress.maximum():
|
||||
if value != self.app_progress.maximum():
|
||||
return
|
||||
|
||||
self.settings.setValue("apps/_meta/isDefaultLoaded", True)
|
||||
logger.info("Default apps loaded")
|
||||
self.default_app_thread.progress.disconnect(on_progress)
|
||||
del self.default_app_progress
|
||||
del self.default_app_thread
|
||||
self.add_app_thread.progress.disconnect(on_progress)
|
||||
del self.app_progress
|
||||
del self.add_app_thread
|
||||
|
||||
self.default_app_progress = QProgressDialog(
|
||||
"Loading default apps", "Cancel", 0, 100
|
||||
)
|
||||
self.app_progress = QProgressDialog("Loading default apps", "Cancel", 0, 100)
|
||||
|
||||
self.default_app_thread = _FetchRegistryThread(self)
|
||||
self.default_app_thread.progress.connect(self.default_app_progress.setValue)
|
||||
self.default_app_thread.progress.connect(on_progress)
|
||||
self.default_app_thread.items.connect(self.default_app_progress.setMaximum)
|
||||
self.default_app_thread.label.connect(self.default_app_progress.setLabelText)
|
||||
self.default_app_progress.canceled.connect(self.default_app_thread.cancel)
|
||||
self.default_app_thread.start()
|
||||
self.add_app_thread = _FetchRegistryThread(self, apps_manifest=REGISTRY_URL)
|
||||
self.add_app_thread.progress.connect(self.app_progress.setValue)
|
||||
self.add_app_thread.progress.connect(on_progress)
|
||||
self.add_app_thread.items.connect(self.app_progress.setMaximum)
|
||||
self.add_app_thread.label.connect(self.app_progress.setLabelText)
|
||||
self.app_progress.canceled.connect(self.add_app_thread.cancel)
|
||||
self.add_app_thread.start()
|
||||
|
||||
self.default_app_progress.show()
|
||||
self.app_progress.show()
|
||||
|
||||
def add_app_ui(self, manifests: List[str]):
|
||||
def on_progress(value):
|
||||
if value != self.app_progress.maximum():
|
||||
return
|
||||
|
||||
self.add_app_thread.progress.disconnect(on_progress)
|
||||
del self.app_progress
|
||||
del self.add_app_thread
|
||||
|
||||
def on_failed(exc_info):
|
||||
msg = QMessageBox(
|
||||
QMessageBox.Critical,
|
||||
"Fail to add application",
|
||||
f"Failed to add application: \n\n{exc_info[0].__name__}: {exc_info[1]}",
|
||||
)
|
||||
msg.setDetailedText("".join(traceback.format_exception(*exc_info)))
|
||||
msg.exec_()
|
||||
|
||||
self.app_progress = QProgressDialog("Installing app", "Cancel", 0, 100)
|
||||
|
||||
self.add_app_thread = _FetchRegistryThread(self, apps=manifests)
|
||||
self.add_app_thread.progress.connect(self.app_progress.setValue)
|
||||
self.add_app_thread.progress.connect(on_progress)
|
||||
self.add_app_thread.items.connect(self.app_progress.setMaximum)
|
||||
self.add_app_thread.label.connect(self.app_progress.setLabelText)
|
||||
self.add_app_thread.failed.connect(on_failed)
|
||||
self.app_progress.canceled.connect(self.add_app_thread.cancel)
|
||||
self.add_app_thread.start()
|
||||
|
||||
self.app_progress.exec_()
|
||||
|
||||
def add_app(self, manifest_url: str, manifest: AppManifest):
|
||||
appid = app_id(manifest_url)
|
||||
@@ -121,7 +179,7 @@ class AppStore(QObject):
|
||||
manifest["appUrl"] = urljoin(manifest_url, manifest["appUrl"])
|
||||
manifest["iconUrl"] = (
|
||||
urljoin(manifest_url, manifest["iconUrl"])
|
||||
if manifest["iconUrl"]
|
||||
if "iconUrl" in manifest and manifest["iconUrl"]
|
||||
else ""
|
||||
)
|
||||
manifest["configUrl"] = urljoin(manifest_url, manifest["configUrl"])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import Qt, Slot, QRect, QObject, QUrl
|
||||
@@ -7,6 +8,7 @@ from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
||||
from PySide2.QtWidgets import QMainWindow
|
||||
|
||||
from runekit.browser import Alt1WebChannel
|
||||
from runekit.ui.game_snap import GameSnapMixin
|
||||
from runekit.ui.windowframe import WindowFrame
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -55,7 +57,7 @@ class PageClass(QWebEnginePage):
|
||||
return super().acceptNavigationRequest(url, type_, is_main_frame)
|
||||
|
||||
|
||||
class BrowserWindow(QMainWindow):
|
||||
class BrowserWindow(GameSnapMixin, QMainWindow):
|
||||
app: "App"
|
||||
browser: QWebEngineView
|
||||
page_class = PageClass
|
||||
@@ -65,7 +67,8 @@ class BrowserWindow(QMainWindow):
|
||||
super().__init__(**kwargs)
|
||||
self.app = app
|
||||
|
||||
if self.framed:
|
||||
if self.framed and sys.platform != "darwin":
|
||||
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.frame = WindowFrame(parent=self)
|
||||
self.frame.on_exit.connect(self.close)
|
||||
self.setCentralWidget(self.frame)
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import QSize, Qt, QThreadPool
|
||||
from PySide2.QtCore import QSize, Qt, QThreadPool, QSettings
|
||||
from PySide2.QtGui import QIcon
|
||||
|
||||
from runekit.app.view.browser_window import BrowserWindow
|
||||
@@ -13,18 +13,20 @@ class AppWindow(BrowserWindow):
|
||||
logger: logging.Logger
|
||||
app_icon: Optional[QIcon]
|
||||
|
||||
framed = sys.platform != "darwin"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# TODO: Hide from taskbar/group this as part of one big window?
|
||||
flags = Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint
|
||||
self.settings = QSettings()
|
||||
super().__init__(**kwargs)
|
||||
self.settings.setParent(self)
|
||||
|
||||
# TODO: Hide from taskbar/group this as part of one big window?
|
||||
flags = Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint | Qt.Window
|
||||
if self.framed:
|
||||
flags |= Qt.CustomizeWindowHint
|
||||
self.setWindowFlags(flags)
|
||||
|
||||
super().__init__(flags=flags, **kwargs)
|
||||
self.pool = QThreadPool(parent=self)
|
||||
self.setWindowTitle(self.app.manifest["appName"])
|
||||
self.snap_to_game()
|
||||
|
||||
self.logger = logging.getLogger(
|
||||
__name__
|
||||
@@ -40,6 +42,10 @@ class AppWindow(BrowserWindow):
|
||||
if self.app_icon:
|
||||
self.setWindowIcon(self.app_icon)
|
||||
|
||||
@property
|
||||
def framed(self) -> bool:
|
||||
return self.settings.value("settings/styledBorder", "true") == "true"
|
||||
|
||||
def minimumSize(self) -> QSize:
|
||||
return QSize(self.app.manifest["minWidth"], self.app.manifest["minHeight"])
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ from PySide2.QtGui import QGuiApplication, QCursor, QScreen
|
||||
from PySide2.QtWebChannel import QWebChannel
|
||||
from PySide2.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlRequestJob
|
||||
|
||||
from runekit.alt1.schema import CaptureMulti
|
||||
from runekit.browser.overlay import OverlayApi
|
||||
from runekit.browser.utils import (
|
||||
ApiPermissionDeniedException,
|
||||
@@ -31,7 +30,6 @@ from runekit.browser.utils import (
|
||||
decode_image,
|
||||
)
|
||||
from runekit.game.instance import ImageType
|
||||
from runekit.image.np_utils import ensure_np_image, np_crop
|
||||
from runekit.ui.tray import tray_icon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import abc
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
from PySide2.QtCore import QObject, Property, Signal
|
||||
|
||||
@@ -12,8 +12,12 @@ class GameManager(QObject):
|
||||
"""Return a list of active game instance. The instances returned should be stable (same instance for all invocation)"""
|
||||
...
|
||||
|
||||
instance_added = Signal()
|
||||
instance_removed = Signal()
|
||||
@abc.abstractmethod
|
||||
def get_active_instance(self) -> Union[GameInstance, None]:
|
||||
...
|
||||
|
||||
instance_added = Signal(GameInstance)
|
||||
instance_removed = Signal(GameInstance)
|
||||
instance_changed = Signal()
|
||||
instances = Property(list, get_instances, notify=instance_changed)
|
||||
|
||||
|
||||
+26
-1
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Callable, Tuple, Dict
|
||||
|
||||
from PySide2.QtCore import Qt, QRect
|
||||
import numpy as np
|
||||
from PySide2.QtCore import Qt, QRect, QTimer
|
||||
from PySide2.QtGui import QGuiApplication, QPen
|
||||
from PySide2.QtWidgets import (
|
||||
QMainWindow,
|
||||
@@ -10,6 +12,8 @@ from PySide2.QtWidgets import (
|
||||
QGraphicsRectItem,
|
||||
)
|
||||
|
||||
from .qt import qpixmap_to_np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .instance import GameInstance
|
||||
|
||||
@@ -27,9 +31,11 @@ class DesktopWideOverlay(QMainWindow):
|
||||
| Qt.WindowTransparentForInput
|
||||
| Qt.WindowStaysOnTopHint
|
||||
)
|
||||
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
self.setAttribute(Qt.WA_NoSystemBackground, True)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
self.setStyleSheet("background: transparent")
|
||||
self._instances = {}
|
||||
|
||||
virtual_screen = QRect(0, 0, 0, 0)
|
||||
@@ -88,3 +94,22 @@ class DesktopWideOverlay(QMainWindow):
|
||||
rect = self._instances[instance.wid]
|
||||
rect.setRect(0, 0, pos.width(), pos.height())
|
||||
rect.setPos(pos.x(), pos.y())
|
||||
|
||||
def check_compatibility(self):
|
||||
QTimer.singleShot(300, self._check_compatibility)
|
||||
|
||||
def _check_compatibility(self):
|
||||
# If we cause black screen then hide ourself out of shame...
|
||||
screenshot = QGuiApplication.primaryScreen().grabWindow(0)
|
||||
image = qpixmap_to_np(screenshot)
|
||||
black_pixels = np.count_nonzero(np.all(image[:, :, :3] == [0, 0, 0], axis=-1))
|
||||
total_pixels = len(image[:, :, 0].flatten())
|
||||
black_percent = black_pixels / total_pixels
|
||||
|
||||
self.logger.debug("Screen black ratio %.2f%%", black_percent * 100)
|
||||
if black_percent > 0.95:
|
||||
self.logger.warning(
|
||||
"Detected black screen at %.2f%%. Disabling overlay",
|
||||
black_percent * 100,
|
||||
)
|
||||
self.hide()
|
||||
File renamed without changes.
+1
-1
@@ -12,7 +12,7 @@ _debug_dump_file = False
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def qpixmap_to_np(im: QPixmap):
|
||||
def qpixmap_to_np(im: QPixmap) -> np.ndarray:
|
||||
# from PIL.ImageQt.fromqimage
|
||||
buffer = QBuffer()
|
||||
buffer.open(QIODevice.ReadWrite)
|
||||
|
||||
@@ -12,7 +12,7 @@ import ApplicationServices
|
||||
from PySide2.QtWidgets import QGraphicsItem
|
||||
|
||||
from ..instance import GameInstance
|
||||
from ..psutil import PsUtilNetStat
|
||||
from ..psutil_mixins import PsUtilNetStat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import QuartzGameManager
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import time
|
||||
from functools import reduce
|
||||
from typing import List, Dict, Optional
|
||||
from typing import List, Dict, Optional, Union
|
||||
|
||||
import Quartz
|
||||
import ApplicationServices
|
||||
@@ -61,9 +61,14 @@ class QuartzGameManager(GameManager):
|
||||
|
||||
def _setup_overlay(self):
|
||||
self.overlay = DesktopWideOverlay()
|
||||
|
||||
def start():
|
||||
self.overlay.show()
|
||||
self.overlay.check_compatibility()
|
||||
|
||||
# Seems like QGraphicsView has a delay before applying stylesheet
|
||||
# Put some delay to allow it to initialize and not flash
|
||||
QTimer.singleShot(1000, lambda: self.overlay.show())
|
||||
QTimer.singleShot(1000, start)
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
@@ -88,6 +93,12 @@ class QuartzGameManager(GameManager):
|
||||
|
||||
return list(self._instances.values())
|
||||
|
||||
def get_active_instance(self) -> Union[GameInstance, None]:
|
||||
if not self._instances:
|
||||
return None
|
||||
|
||||
return self._instances.values()[0]
|
||||
|
||||
def get_instance_by_pid(self, pid: int) -> Optional[QuartzGameInstance]:
|
||||
for instance in self._instances.values():
|
||||
if instance.pid == pid:
|
||||
|
||||
@@ -9,7 +9,7 @@ from PySide2.QtGui import QWindow, QGuiApplication
|
||||
from PySide2.QtWidgets import QGraphicsItem
|
||||
|
||||
from runekit.game.instance import GameInstance
|
||||
from runekit.game.psutil import PsUtilNetStat
|
||||
from runekit.game.psutil_mixins import PsUtilNetStat
|
||||
from runekit.game.qt import QtGrabMixin, QtEmbedMixin
|
||||
from .ximage import zpixmap_shm_to_image
|
||||
|
||||
@@ -31,6 +31,7 @@ class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
|
||||
|
||||
input_signal = Signal(xcffib.Event)
|
||||
config_signal = Signal(xcffib.Event)
|
||||
destroy_signal = Signal(xcffib.Event)
|
||||
|
||||
def __init__(self, manager: "X11GameManager", wid: int, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -53,7 +54,7 @@ class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
|
||||
self.wid, xcffib.composite.Redirect.Automatic
|
||||
)
|
||||
self.name_pixmap()
|
||||
self.manager.connection.core.ChangeWindowAttributes(
|
||||
self.manager.connection.core.ChangeWindowAttributesChecked(
|
||||
self.wid,
|
||||
xcffib.xproto.CW.EventMask,
|
||||
[
|
||||
@@ -96,14 +97,20 @@ class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
|
||||
return self.cached_position
|
||||
|
||||
def get_scaling(self) -> float:
|
||||
screen = QGuiApplication.screenAt(self.get_position().topLeft())
|
||||
pos = self.get_position().topLeft()
|
||||
if pos.x() < 0:
|
||||
pos.setX(0)
|
||||
if pos.y() < 0:
|
||||
pos.setY(0)
|
||||
|
||||
screen = QGuiApplication.screenAt(pos)
|
||||
return screen.devicePixelRatio()
|
||||
|
||||
def is_focused(self) -> bool:
|
||||
return self.is_focused
|
||||
return self._is_focused
|
||||
|
||||
def _update_is_focused(self):
|
||||
self.is_focused = self.manager.get_active_window() == self.wid
|
||||
self._is_focused = self.manager.get_active_window() == self.wid
|
||||
|
||||
def set_taskbar_progress(self, type_, progress):
|
||||
# TODO: Implement this on Unity? I don't use Unity
|
||||
|
||||
+93
-22
@@ -9,7 +9,7 @@ import xcffib.composite
|
||||
import xcffib.shm
|
||||
import xcffib.xinput
|
||||
import xcffib.xproto
|
||||
from PySide2.QtCore import QThread, Slot, QObject
|
||||
from PySide2.QtCore import QThread, Slot, QObject, Signal
|
||||
|
||||
from runekit.game import GameManager
|
||||
from .instance import GameInstance, X11GameInstance
|
||||
@@ -33,6 +33,7 @@ class X11GameManager(GameManager):
|
||||
self._atom = {}
|
||||
self._shm = []
|
||||
|
||||
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
self.connection = xcffib.Connection()
|
||||
self.screen = self.connection.get_screen_pointers()[self.connection.pref_screen]
|
||||
self.xcomposite = self.connection(xcffib.composite.key)
|
||||
@@ -47,9 +48,13 @@ class X11GameManager(GameManager):
|
||||
self.event_worker.moveToThread(self.event_thread)
|
||||
self.event_thread.started.connect(self.event_worker.run)
|
||||
self.event_thread.finished.connect(self.event_worker.deleteLater)
|
||||
self.event_worker.create_signal.connect(self.on_game_opened)
|
||||
self.event_worker.destroy_signal.connect(self.on_game_closed)
|
||||
|
||||
self.event_thread.start()
|
||||
|
||||
self.get_instances()
|
||||
|
||||
def stop(self):
|
||||
self.event_thread.requestInterruption()
|
||||
self.event_thread.quit()
|
||||
@@ -57,29 +62,56 @@ class X11GameManager(GameManager):
|
||||
|
||||
def get_instances(self) -> List[GameInstance]:
|
||||
def visit(wid: int):
|
||||
wm_class = self.get_property(wid, xcffib.xproto.Atom.WM_CLASS)
|
||||
if self.is_game(wid):
|
||||
if wid not in self._instances:
|
||||
self.logger.info("Found game instance %d", wid)
|
||||
instance = X11GameInstance(self, wid, parent=self)
|
||||
self._instances[wid] = instance
|
||||
|
||||
if wm_class:
|
||||
instance_name, app_name = wm_class.split("\00")
|
||||
if app_name == WM_APP_NAME:
|
||||
if wid not in self._instances:
|
||||
instance = X11GameInstance(self, wid, parent=self)
|
||||
self._instances[wid] = instance
|
||||
|
||||
query = self.connection.core.QueryTree(wid).reply()
|
||||
for child in query.children:
|
||||
visit(child)
|
||||
try:
|
||||
query = self.connection.core.QueryTree(wid).reply()
|
||||
for child in query.children:
|
||||
visit(child)
|
||||
except xcffib.xproto.WindowError:
|
||||
return
|
||||
|
||||
visit(self.screen.root)
|
||||
|
||||
return list(self._instances.values())
|
||||
|
||||
def get_active_instance(self) -> Union[GameInstance, None]:
|
||||
for instance in self._instances.values():
|
||||
if instance.is_focused():
|
||||
return instance
|
||||
|
||||
return None
|
||||
|
||||
def is_game(self, wid: int) -> bool:
|
||||
geom_req = self.connection.core.GetGeometry(wid)
|
||||
try:
|
||||
wm_class = self.get_property(wid, xcffib.xproto.Atom.WM_CLASS)
|
||||
except xcffib.xproto.WindowError:
|
||||
geom_req.discard_reply()
|
||||
return False
|
||||
|
||||
geom = geom_req.reply()
|
||||
if geom.width == 32 and geom.height == 32:
|
||||
# OpenGL test window
|
||||
return False
|
||||
|
||||
if not wm_class:
|
||||
return False
|
||||
|
||||
instance_name, app_name = wm_class.split("\00")
|
||||
return app_name == WM_APP_NAME
|
||||
|
||||
def get_active_window(self) -> int:
|
||||
return self.get_property(self.screen.root, "_NET_ACTIVE_WINDOW")
|
||||
|
||||
def _setup_overlay(self):
|
||||
self.overlay = DesktopWideOverlay()
|
||||
self.overlay.show()
|
||||
self.overlay.check_compatibility()
|
||||
|
||||
def _setup_composite(self):
|
||||
self.xcomposite.QueryVersion(0, 4, is_checked=True)
|
||||
@@ -147,8 +179,27 @@ class X11GameManager(GameManager):
|
||||
shm.detach()
|
||||
shm.remove()
|
||||
|
||||
@Slot(xcffib.Event)
|
||||
def on_game_opened(self, evt: xcffib.xproto.CreateNotifyEvent):
|
||||
self.logger.info("New game window opened %d", evt.window)
|
||||
self._instances[evt.window] = X11GameInstance(self, evt.window, parent=self)
|
||||
|
||||
@Slot(int)
|
||||
def on_game_closed(self, wid: int):
|
||||
if wid not in self._instances:
|
||||
return
|
||||
|
||||
self.logger.info("Game window %d closed", wid)
|
||||
instance = self._instances[wid]
|
||||
del self._instances[wid]
|
||||
self.instance_removed.emit(instance)
|
||||
self.instance_changed.emit()
|
||||
|
||||
|
||||
class X11EventWorker(QObject):
|
||||
create_signal = Signal(xcffib.Event)
|
||||
destroy_signal = Signal(int)
|
||||
|
||||
def __init__(self, manager: "X11GameManager", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.manager = manager
|
||||
@@ -159,17 +210,21 @@ class X11EventWorker(QObject):
|
||||
xcffib.xproto.ConfigureNotifyEvent: self.on_configure_event,
|
||||
xcffib.xproto.KeyPressEvent: self.on_input_event,
|
||||
xcffib.xproto.ButtonPressEvent: self.on_input_event,
|
||||
xcffib.xproto.CreateNotifyEvent: self.on_create,
|
||||
xcffib.xproto.DestroyNotifyEvent: self.on_destroy,
|
||||
}
|
||||
self.active_win_id = self.manager.get_active_window()
|
||||
|
||||
@Slot()
|
||||
def run(self):
|
||||
self.manager.connection.core.ChangeWindowAttributes(
|
||||
self.manager.connection.core.ChangeWindowAttributesChecked(
|
||||
self.manager.screen.root,
|
||||
xcffib.xproto.CW.EventMask,
|
||||
[xcffib.xproto.EventMask.PropertyChange],
|
||||
is_checked=True,
|
||||
)
|
||||
[
|
||||
xcffib.xproto.EventMask.PropertyChange
|
||||
| xcffib.xproto.EventMask.SubstructureNotify,
|
||||
],
|
||||
).check()
|
||||
current_thread = QThread.currentThread()
|
||||
|
||||
while True:
|
||||
@@ -191,7 +246,11 @@ class X11EventWorker(QObject):
|
||||
)
|
||||
|
||||
def on_property_change(self, evt: xcffib.xproto.PropertyNotifyEvent):
|
||||
if evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW):
|
||||
if (
|
||||
evt.state == xcffib.xproto.Property.NewValue
|
||||
and evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW)
|
||||
and evt.window == self.manager.screen.root
|
||||
):
|
||||
active_win_id = self.manager.get_active_window()
|
||||
|
||||
if self.active_win_id == active_win_id:
|
||||
@@ -202,8 +261,8 @@ class X11EventWorker(QObject):
|
||||
for id_, instance in self.manager._instances.items():
|
||||
active = active_win_id == id_
|
||||
|
||||
if active != instance.is_focused:
|
||||
instance.is_focused = active
|
||||
if active != instance._is_focused:
|
||||
instance._is_focused = active
|
||||
instance.focusChanged.emit(active)
|
||||
|
||||
def on_input_event(
|
||||
@@ -218,6 +277,18 @@ class X11EventWorker(QObject):
|
||||
try:
|
||||
self.manager._instances[evt.window].config_signal.emit(evt)
|
||||
except KeyError:
|
||||
self.logger.debug(
|
||||
"Got configure event for %d but is not registered", evt.window
|
||||
)
|
||||
pass
|
||||
|
||||
def on_create(self, evt: xcffib.xproto.CreateNotifyEvent):
|
||||
if evt.window in self.manager._instances:
|
||||
return
|
||||
|
||||
# At this point we aren't sure if it's actually the game...
|
||||
if not self.manager.is_game(evt.window):
|
||||
return
|
||||
|
||||
self.create_signal.emit(evt)
|
||||
|
||||
def on_destroy(self, evt: xcffib.xproto.DestroyNotifyEvent):
|
||||
if evt.window in self.manager._instances:
|
||||
self.destroy_signal.emit(evt.window)
|
||||
+29
-19
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from PySide2.QtCore import Slot, QCoreApplication
|
||||
@@ -22,6 +23,7 @@ class Host:
|
||||
|
||||
def __init__(self, manager: "GameManager"):
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
self.manager = manager
|
||||
self.open_app = []
|
||||
self.notifier = AutoNotifier()
|
||||
@@ -33,19 +35,11 @@ class Host:
|
||||
QCoreApplication.instance().aboutToQuit.connect(self.on_before_quit)
|
||||
self.app_store.app_change.connect(self.tray_icon.update_menu)
|
||||
self.tray_icon.on_settings.connect(self.open_settings)
|
||||
self.manager.instance_removed.connect(self.on_game_quit)
|
||||
|
||||
def on_before_quit(self):
|
||||
for app in self.open_app:
|
||||
# FIXME: Why app don't cleanup after themselves?
|
||||
if app.window:
|
||||
app.window.deleteLater()
|
||||
app.window = None
|
||||
if app.alt1api:
|
||||
app.alt1api.deleteLater()
|
||||
app.alt1api = None
|
||||
if app.web_profile:
|
||||
app.web_profile.deleteLater()
|
||||
app.web_profile = None
|
||||
app.close()
|
||||
|
||||
def __del__(self):
|
||||
self.open_app = []
|
||||
@@ -70,21 +64,25 @@ class Host:
|
||||
self.launch_app(appid, manifest)
|
||||
|
||||
def launch_app(self, appid: str, manifest: AppManifest):
|
||||
instances = self.manager.get_instances()
|
||||
instance = self.manager.get_active_instance()
|
||||
|
||||
if len(instances) == 0:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"No game instances found",
|
||||
"Cannot find open RuneScape window. Please launch the game before any apps",
|
||||
)
|
||||
return
|
||||
if not instance:
|
||||
instances = self.manager.get_instances()
|
||||
if not instances:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"No game instances found",
|
||||
"Cannot find open RuneScape window. Please launch the game before any apps",
|
||||
)
|
||||
return
|
||||
|
||||
instance = instances[0]
|
||||
|
||||
app = App(
|
||||
host=self,
|
||||
app_id=appid,
|
||||
manifest=manifest,
|
||||
game_instance=instances[0],
|
||||
game_instance=instance,
|
||||
source_url=manifest["configUrl"],
|
||||
)
|
||||
|
||||
@@ -104,3 +102,15 @@ class Host:
|
||||
def open_settings(self):
|
||||
self.setting_dialog.show()
|
||||
self.setting_dialog.raise_()
|
||||
|
||||
@Slot()
|
||||
def on_game_quit(self, instance: "GameInstance"):
|
||||
self.logger.info("Game instance is closing")
|
||||
for app in self.open_app[:]:
|
||||
if app.game_instance == instance:
|
||||
self.logger.debug(
|
||||
"Closing app window %s because instance is closing",
|
||||
app.manifest["appName"],
|
||||
)
|
||||
app.close()
|
||||
self.open_app.remove(app)
|
||||
+50
-21
@@ -2,7 +2,7 @@ import json
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import QSize, Qt, Slot, QTimer
|
||||
from PySide2.QtCore import QSize, Qt, Slot, QTimer, QSettings
|
||||
from PySide2.QtGui import QIcon
|
||||
from PySide2.QtWidgets import (
|
||||
QTabWidget,
|
||||
@@ -24,7 +24,7 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from .appstore_model import AppStoreModel
|
||||
from ..ui import TooltipNotifier, TrayIconNotifier
|
||||
from ..ui import TooltipNotifier, TrayIconNotifier, AutoNotifier
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .host import Host
|
||||
@@ -76,6 +76,7 @@ class ApplicationPage(QWidget):
|
||||
|
||||
add_button = QPushButton(QIcon.fromTheme("list-add"), "", self)
|
||||
add_button.setToolTip("Add application")
|
||||
add_button.clicked.connect(self.on_add)
|
||||
buttons.addWidget(add_button)
|
||||
|
||||
mkdir_button = QPushButton(QIcon.fromTheme("folder-new"), "", self)
|
||||
@@ -90,6 +91,16 @@ class ApplicationPage(QWidget):
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def on_add(self):
|
||||
dialog = QInputDialog(self)
|
||||
dialog.setLabelText("Enter appconfig.json URL")
|
||||
|
||||
if dialog.exec_() == QInputDialog.Rejected:
|
||||
return
|
||||
|
||||
app_url = dialog.textValue().strip()
|
||||
self.host.app_store.add_app_ui([app_url])
|
||||
|
||||
def on_mkdir(self):
|
||||
dialog = QInputDialog(self)
|
||||
dialog.setLabelText("Directory name")
|
||||
@@ -143,43 +154,51 @@ class InterfacePage(QWidget):
|
||||
def __init__(self, host: "Host", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.host = host
|
||||
self.settings = QSettings(self)
|
||||
self._layout()
|
||||
|
||||
def _layout(self):
|
||||
layout = QFormLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addRow(
|
||||
None,
|
||||
QLabel("RuneKit needs to be restarted for changes to take effect", self),
|
||||
)
|
||||
|
||||
tooltip_label = QLabel("Display tooltips as", self)
|
||||
tooltip_field = QComboBox(self)
|
||||
tooltip_field.insertItem(0, "Disabled")
|
||||
tooltip_field.insertItem(1, "Notification") # XXX: Check support
|
||||
tooltip_field.insertItem(2, "Cursor tooltip")
|
||||
|
||||
selected = int(
|
||||
self.settings.value("settings/tooltip", AutoNotifier.METHOD_NOTIFICATION)
|
||||
)
|
||||
selected_idx = 0
|
||||
for idx, method in enumerate(AutoNotifier.availableMethods().items()):
|
||||
tooltip_field.insertItem(idx, method[1])
|
||||
if method[0] == selected:
|
||||
selected_idx = idx
|
||||
|
||||
tooltip_field.setCurrentIndex(selected_idx)
|
||||
|
||||
tooltip_field.currentIndexChanged.connect(self.preview_tooltip)
|
||||
tooltip_field.currentIndexChanged.connect(self.on_tooltip_change)
|
||||
layout.addRow(tooltip_label, tooltip_field)
|
||||
|
||||
border_field = QCheckBox("Styled window border", self)
|
||||
border_field.setDisabled(sys.platform == "darwin")
|
||||
border_field.setChecked(sys.platform != "darwin")
|
||||
border_field.setChecked(
|
||||
(self.settings.value("settings/styledBorder", "true") == "true")
|
||||
and sys.platform != "darwin"
|
||||
)
|
||||
border_field.stateChanged.connect(self.on_change_styled_border)
|
||||
layout.addRow(None, border_field)
|
||||
|
||||
# I think this doesn't belong to "Interface"
|
||||
# either rename it to settings or make separate game tab
|
||||
capturei_label = QLabel("Capture interval", self)
|
||||
capturei_layout = QHBoxLayout()
|
||||
capturei_field = QSpinBox(self)
|
||||
capturei_field.setRange(100, 2000)
|
||||
capturei_field.setSingleStep(100)
|
||||
capturei_field.setValue(100)
|
||||
capturei_layout.addWidget(capturei_field, 1)
|
||||
capturei_unit = QLabel("ms", self)
|
||||
capturei_layout.addWidget(capturei_unit, 0)
|
||||
layout.addRow(capturei_label, capturei_layout)
|
||||
|
||||
@Slot(int)
|
||||
def preview_tooltip(self, index: int):
|
||||
if index == 1:
|
||||
type_ = list(AutoNotifier.availableMethods().items())[index][0]
|
||||
if type_ == AutoNotifier.METHOD_NOTIFICATION:
|
||||
notifier = TrayIconNotifier()
|
||||
elif index == 2:
|
||||
elif type_ == AutoNotifier.METHOD_TOOLTIP:
|
||||
notifier = TooltipNotifier()
|
||||
else:
|
||||
return
|
||||
@@ -190,3 +209,13 @@ class InterfacePage(QWidget):
|
||||
self._last_notifier = notifier
|
||||
notifier.notify("This is an example notification")
|
||||
QTimer.singleShot(3000, lambda: notifier.notify(""))
|
||||
|
||||
@Slot(int)
|
||||
def on_tooltip_change(self, index: int):
|
||||
idx = list(AutoNotifier.availableMethods().items())[index]
|
||||
self.settings.setValue("settings/tooltip", idx[0])
|
||||
|
||||
@Slot(int)
|
||||
def on_change_styled_border(self, state: int):
|
||||
checked = state == Qt.Checked
|
||||
self.settings.setValue("settings/styledBorder", checked)
|
||||
@@ -0,0 +1,82 @@
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import click
|
||||
from PySide2.QtCore import QSettings, Qt, QTimer
|
||||
from PySide2.QtWidgets import (
|
||||
QApplication,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
import runekit._resources
|
||||
from runekit import browser
|
||||
from runekit.game import get_platform_manager
|
||||
from runekit.host import Host
|
||||
|
||||
|
||||
@click.command(
|
||||
context_settings=dict(
|
||||
ignore_unknown_options=True,
|
||||
)
|
||||
)
|
||||
@click.option("--game-index", default=0, help="Game instance index, starting from 0")
|
||||
@click.argument("qt_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@click.argument("app_url", required=False)
|
||||
def main(app_url, game_index, qt_args):
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logging.info("Starting QtWebEngine")
|
||||
browser.init()
|
||||
|
||||
app = QApplication(["runekit", *qt_args])
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
app.setOrganizationName("cupco.de")
|
||||
app.setOrganizationDomain("cupco.de")
|
||||
app.setApplicationName("RuneKit")
|
||||
|
||||
signal.signal(signal.SIGINT, lambda no, frame: app.quit())
|
||||
|
||||
timer = QTimer()
|
||||
timer.start(300)
|
||||
timer.timeout.connect(lambda: None)
|
||||
|
||||
QSettings.setDefaultFormat(QSettings.IniFormat)
|
||||
|
||||
try:
|
||||
game_manager = get_platform_manager()
|
||||
host = Host(game_manager)
|
||||
|
||||
if app_url == "settings":
|
||||
host.open_settings()
|
||||
host.setting_dialog.setAttribute(Qt.WA_DeleteOnClose)
|
||||
host.setting_dialog.destroyed.connect(app.quit)
|
||||
elif app_url:
|
||||
logging.info("Loading app")
|
||||
game_app = host.launch_app_from_url(app_url)
|
||||
game_app.window.destroyed.connect(app.quit)
|
||||
else:
|
||||
if not host.app_store.has_default_apps():
|
||||
host.app_store.load_default_apps()
|
||||
|
||||
app.exec_()
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
msg = QMessageBox(
|
||||
QMessageBox.Critical,
|
||||
"Oh No!",
|
||||
f"Fatal error: \n\n{e.__class__.__name__}: {e}",
|
||||
)
|
||||
msg.setDetailedText(traceback.format_exc())
|
||||
msg.exec_()
|
||||
|
||||
raise
|
||||
finally:
|
||||
if game_manager is not None:
|
||||
logging.debug("Stopping game manager")
|
||||
game_manager.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import QRect
|
||||
from PySide2.QtWidgets import QMainWindow
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from runekit.app import App
|
||||
|
||||
|
||||
class GameSnapMixin(QMainWindow):
|
||||
app: "App"
|
||||
__last_game_pos: QRect
|
||||
|
||||
def snap_to_game(self):
|
||||
rect = self.app.game_instance.get_position()
|
||||
pos = rect.topLeft()
|
||||
if pos.x() < 0:
|
||||
pos.setX(0)
|
||||
if pos.y() < 0:
|
||||
pos.setY(0)
|
||||
|
||||
self.move(pos)
|
||||
self.__last_game_pos = self.app.game_instance.get_position()
|
||||
self.app.game_instance.positionChanged.connect(self._update_game_snap)
|
||||
|
||||
def _update_game_snap(self, game_pos: QRect):
|
||||
# If the top left spot of us fall into game window, then we move with the game
|
||||
tl = self.geometry().topLeft()
|
||||
if not game_pos.contains(tl):
|
||||
self.__last_game_pos = game_pos
|
||||
return
|
||||
|
||||
dx = game_pos.x() - self.__last_game_pos.x()
|
||||
dy = game_pos.y() - self.__last_game_pos.y()
|
||||
|
||||
pos = self.pos()
|
||||
pos.setX(pos.x() + dx)
|
||||
pos.setY(pos.y() + dy)
|
||||
self.move(pos)
|
||||
|
||||
self.__last_game_pos = game_pos
|
||||
+37
-7
@@ -1,6 +1,7 @@
|
||||
import abc
|
||||
from typing import Union
|
||||
from typing import Union, Dict
|
||||
|
||||
from PySide2.QtCore import QObject, QSettings
|
||||
from PySide2.QtWidgets import QSystemTrayIcon
|
||||
|
||||
from .tooltip import TooltipManager
|
||||
@@ -13,6 +14,11 @@ class Notifier(abc.ABC):
|
||||
...
|
||||
|
||||
|
||||
class StubNotifier(Notifier):
|
||||
def notify(self, text):
|
||||
pass
|
||||
|
||||
|
||||
class TooltipNotifier(Notifier):
|
||||
def __init__(self):
|
||||
self.manager = TooltipManager()
|
||||
@@ -27,17 +33,41 @@ class TrayIconNotifier(Notifier):
|
||||
tray_icon().showMessage("RuneKit", text)
|
||||
|
||||
|
||||
class AutoNotifier(Notifier):
|
||||
class AutoNotifier(QObject):
|
||||
notifier: Notifier
|
||||
|
||||
METHOD_STUB = 0
|
||||
METHOD_NOTIFICATION = 1
|
||||
METHOD_TOOLTIP = 2
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.settings = QSettings(self)
|
||||
type_ = int(self.settings.value("settings/tooltip", self.METHOD_NOTIFICATION))
|
||||
if (
|
||||
QSystemTrayIcon.isSystemTrayAvailable()
|
||||
and QSystemTrayIcon.supportsMessages()
|
||||
and type_ == self.METHOD_NOTIFICATION
|
||||
):
|
||||
self.notifier = TrayIconNotifier()
|
||||
elif type_ == self.METHOD_TOOLTIP:
|
||||
self.notifier = TooltipNotifier()
|
||||
else:
|
||||
self.notifier = StubNotifier()
|
||||
|
||||
def notify(self, text):
|
||||
self.notifier.notify(text)
|
||||
|
||||
@classmethod
|
||||
def availableMethods(cls) -> Dict[int, str]:
|
||||
out = {
|
||||
cls.METHOD_STUB: "Disabled",
|
||||
cls.METHOD_TOOLTIP: "Cursor tooltip",
|
||||
}
|
||||
if (
|
||||
QSystemTrayIcon.isSystemTrayAvailable()
|
||||
and QSystemTrayIcon.supportsMessages()
|
||||
):
|
||||
self.notifier = TrayIconNotifier()
|
||||
else:
|
||||
self.notifier = TooltipNotifier()
|
||||
out[cls.METHOD_NOTIFICATION] = "Notification"
|
||||
|
||||
def notify(self, text):
|
||||
self.notifier.notify(text)
|
||||
return out
|
||||
@@ -52,6 +52,7 @@ class TrayIcon(QSystemTrayIcon):
|
||||
app_menu.triggered.connect(
|
||||
lambda _=None, app_id=app_id: self.host.launch_app_id(app_id)
|
||||
)
|
||||
app_menu.setToolTip(manifest["description"])
|
||||
|
||||
icon = self.host.app_store.icon(app_id)
|
||||
if icon:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtCore import Signal, QPoint
|
||||
from PySide2.QtGui import QPixmap, Qt, QResizeEvent
|
||||
from PySide2.QtWidgets import (
|
||||
QWidget,
|
||||
@@ -18,6 +18,7 @@ class WindowFrame(QWidget):
|
||||
on_exit = Signal()
|
||||
|
||||
content: QWidget
|
||||
shade = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -28,6 +29,7 @@ class WindowFrame(QWidget):
|
||||
self._inner = _FrameInner(parent=self)
|
||||
self._buttons = _WindowButtons(parent=self)
|
||||
self._buttons.on_minimize.connect(self.on_minimize)
|
||||
self._buttons.on_minimize.connect(self.handle_shade)
|
||||
self._buttons.on_settings.connect(self.on_settings)
|
||||
self._buttons.on_exit.connect(self.on_exit)
|
||||
|
||||
@@ -51,6 +53,41 @@ class WindowFrame(QWidget):
|
||||
self._inner.set_content(content)
|
||||
self.content = content
|
||||
|
||||
def handle_shade(self):
|
||||
self.set_shade(not self.shade)
|
||||
|
||||
def set_shade(self, shade: bool):
|
||||
if shade == self.shade:
|
||||
return
|
||||
|
||||
self.shade = shade
|
||||
|
||||
if self.shade:
|
||||
self.previous_window_geom = self.window().size()
|
||||
self.previous_window_min_size = self.window().minimumSize()
|
||||
top_right = self.window().geometry().topRight()
|
||||
self._inner.hide()
|
||||
self.window().setMinimumSize(self._buttons.sizeHint())
|
||||
|
||||
size = self._buttons.sizeHint()
|
||||
self.window().resize(size)
|
||||
|
||||
new_top_left = QPoint(top_right)
|
||||
new_top_left.setX(top_right.x() - size.width())
|
||||
self.window().move(new_top_left)
|
||||
else:
|
||||
pos = self.window().geometry().topRight()
|
||||
self._inner.show()
|
||||
if hasattr(self, "previous_window_geom"):
|
||||
self.window().setMinimumSize(self.previous_window_min_size)
|
||||
geom = self.previous_window_geom
|
||||
else:
|
||||
geom = self.window().sizeHint()
|
||||
|
||||
pos.setX(pos.x() - geom.width())
|
||||
self.window().resize(geom)
|
||||
self.window().move(pos)
|
||||
|
||||
|
||||
class _FrameInner(QWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -138,12 +175,12 @@ class _WindowButtons(QWidget):
|
||||
)
|
||||
layout.addWidget(minimize_button, alignment=Qt.AlignTop)
|
||||
|
||||
settings_button = QPushButton(self)
|
||||
settings_button.clicked.connect(self.on_settings)
|
||||
settings_button.setStyleSheet(
|
||||
self.button_style("settings.png", "settingsHover.png")
|
||||
)
|
||||
layout.addWidget(settings_button, alignment=Qt.AlignTop)
|
||||
# settings_button = QPushButton(self)
|
||||
# settings_button.clicked.connect(self.on_settings)
|
||||
# settings_button.setStyleSheet(
|
||||
# self.button_style("settings.png", "settingsHover.png")
|
||||
# )
|
||||
# layout.addWidget(settings_button, alignment=Qt.AlignTop)
|
||||
|
||||
exit_button = QPushButton(self)
|
||||
exit_button.clicked.connect(self.on_exit)
|
||||
|
||||
Reference in new issue
Block a user