1 Commits
Author SHA1 Message Date
Manatsawin Hanmongkolchai 6edf87ba7b OpenCV 2021-02-12 21:44:36 +07:00
43 changed files with 604 additions and 1599 deletions

No files matched your search

-67
View File
@@ -1,67 +0,0 @@
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 }}
-1
View File
@@ -7,4 +7,3 @@ setup.py
__pycache__
runekit/_resources.py
RuneKitApp.spec
*.AppImage
+1 -9
View File
@@ -41,17 +41,9 @@ Packages related to Alt1 app.
## UI
- host: RuneKit Qt root
- host: Main window of RuneKit
- ui: Collection of UI tools
## Porting
With this design it should be simple to port RuneKit to any platform supported by Qt by just reimplementing `game` for that platform
# Settings design
- apps/
- <b2b size 16 of manifest url> (json): App manifest
- _folder
- <folder name>/0 (str): App ID
- _meta
- isDefaultLoaded (bool): Has default apps list downloaded
+5 -32
View File
@@ -1,44 +1,17 @@
LINUXDEPLOY ?= linuxdeploy-$(shell uname -m).AppImage
dev: runekit/_resources.py
dist/runekit.tar.gz: main.py poetry.lock $(wildcard runekit/**/*)
poetry build -f sdist
cd dist; cp runekit-*.tar.gz runekit.tar.gz
runekit/_resources.py: resources.qrc $(wildcard runekit/**/*.js) $(wildcard runekit/**/*.png)
pyside2-rcc $< -o $@
# 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/**/*)
dist/RuneKitApp.app: main.py poetry.lock $(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
+17 -27
View File
@@ -7,39 +7,14 @@ 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)
## 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
## Running
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
@@ -52,6 +27,21 @@ 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.
+14
View File
@@ -0,0 +1,14 @@
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"]})
-16
View File
@@ -1,16 +0,0 @@
<?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>
-7
View File
@@ -1,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=RuneKit
Exec=runekit
Comment=Alt1-compatible toolbox for RuneScape 3
Icon=python
Categories=Game;
-12
View File
@@ -1,12 +0,0 @@
#!/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 $@
+96 -1
View File
@@ -1,4 +1,99 @@
from runekit.main import main
import logging
import sys
import traceback
import click
from PySide2.QtWidgets import (
QApplication,
QMessageBox,
QInputDialog,
QDialog,
QSystemTrayIcon,
)
import runekit._resources
from runekit import browser
from runekit.game import get_platform_manager
from runekit.host import Host
from runekit.ui.tray import TrayIcon
@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)
game_manager = None
try:
if QSystemTrayIcon.isSystemTrayAvailable():
TrayIcon().show()
host = Host()
if app_url == "settings":
settings_wnd = host.open_settings()
settings_wnd.destroyed.connect(app.quit)
else:
game_manager = get_platform_manager()
logging.info("Scanning for game instances")
game_instances = game_manager.get_instances()
logging.info("Found %d instances", len(game_instances))
if len(game_instances) == 0:
QMessageBox(
QMessageBox.Critical,
"Game not found",
"Cannot find RuneScape. Launch the game first",
).exec_()
return
game = game_instances[game_index]
if app_url is None:
dialog = QInputDialog()
dialog.setComboBoxEditable(True)
dialog.setComboBoxItems(
[
"https://runeapps.org/apps/alt1/example/appconfig.json",
"https://runeapps.org/apps/clue/appconfig.json",
"https://runeapps.org/apps/alt1/afkscape/appconfig.json",
]
)
dialog.setLabelText("Enter appconfig URL")
dialog.setWindowTitle("RuneKit")
if dialog.exec_() == QDialog.DialogCode.Rejected:
return
app_url = dialog.textValue()
logging.info("Loading app")
game_app = host.start_app(app_url, game)
game_app.window.destroyed.connect(app.quit)
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:
game_manager.stop()
if __name__ == "__main__":
main()
Generated
+196 -76
View File
@@ -46,7 +46,7 @@ python-versions = "*"
[[package]]
name = "cffi"
version = "1.14.5"
version = "1.14.4"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
@@ -71,6 +71,22 @@ 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 = "future"
version = "0.18.2"
description = "Clean single-source support for Python 3 and 2"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "idna"
version = "2.10"
@@ -106,6 +122,17 @@ category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "opencv-python"
version = "4.5.1.48"
description = "Wrapper package for OpenCV python bindings."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
numpy = ">=1.19.3"
[[package]]
name = "opencv-python-headless"
version = "4.5.1.48"
@@ -125,9 +152,20 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pefile"
version = "2019.4.18"
description = "Python PE parsing module"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
future = "*"
[[package]]
name = "pillow"
version = "8.1.2"
version = "8.1.0"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
@@ -163,7 +201,9 @@ python-versions = ">=3.5"
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2020.6"
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
[package.extras]
encryption = ["tinyaes (>=1.0.0)"]
@@ -171,7 +211,7 @@ hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2021.1"
version = "2020.11"
description = "Community maintained hooks for PyInstaller"
category = "dev"
optional = false
@@ -245,6 +285,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10"
[package.dependencies]
shiboken2 = "5.15.2"
[[package]]
name = "pywin32-ctypes"
version = "0.2.0"
description = ""
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "regex"
version = "2020.11.13"
@@ -347,7 +395,7 @@ six = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9,<3.10"
content-hash = "a6e17c47f17532e7594f59aa7bcb6bfc0b06c3f27c3b50a4a7cb8b6fd829451a"
content-hash = "077116eae8dbd00c752035808cd49c268cc8124c705c841a2da69b68d577adee"
[metadata.files]
altgraph = [
@@ -366,43 +414,42 @@ certifi = [
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
cffi = [
{file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"},
{file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"},
{file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"},
{file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"},
{file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"},
{file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"},
{file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"},
{file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"},
{file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"},
{file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"},
{file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"},
{file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"},
{file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"},
{file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
{file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
{file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
{file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
{file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
{file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
{file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
{file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
{file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
{file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
{file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
{file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
{file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
{file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
{file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
{file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
{file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
{file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
{file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
{file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
{file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
{file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
{file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
{file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
{file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"},
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"},
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"},
{file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"},
{file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"},
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"},
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"},
{file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"},
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"},
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"},
{file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"},
{file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"},
{file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"},
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"},
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"},
{file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"},
{file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"},
{file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"},
{file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"},
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"},
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"},
{file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"},
{file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"},
{file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"},
{file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"},
{file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"},
{file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"},
{file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"},
{file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"},
{file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"},
{file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"},
{file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"},
{file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"},
{file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"},
{file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"},
{file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
@@ -412,6 +459,46 @@ 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"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
@@ -450,6 +537,33 @@ numpy = [
{file = "numpy-1.20.1-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e"},
{file = "numpy-1.20.1.zip", hash = "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a"},
]
opencv-python = [
{file = "opencv-python-4.5.1.48.tar.gz", hash = "sha256:78a6db8467639383caedf1d111da3510a4ee1a0aacf2117821cae2ee8f92ce37"},
{file = "opencv_python-4.5.1.48-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:bcb27773cfd5340b2b599b303d9f5499838ef4780c20c038f6030175408c64df"},
{file = "opencv_python-4.5.1.48-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9646875c501788b1b098f282d777b667d6da69801739504f1b2fd1268970d1da"},
{file = "opencv_python-4.5.1.48-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:ebe83901971a6755512424c4fe9f63341cca501b7c497bf608dd38ee31ba3f4c"},
{file = "opencv_python-4.5.1.48-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:d8aefcb30b71064dbbaa2b0ace161a36464c29375a83998fbda39a1d1740f942"},
{file = "opencv_python-4.5.1.48-cp36-cp36m-win32.whl", hash = "sha256:32dee1c9fd3e31e28edef7b56f868e2b40e280b7062304f9fb8a14dbc51547d5"},
{file = "opencv_python-4.5.1.48-cp36-cp36m-win_amd64.whl", hash = "sha256:9c77d508e6822f1f40c727d21b822d017622d8305dce7eccf0ab06caac16d5c6"},
{file = "opencv_python-4.5.1.48-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:4982fa8ccc38310a2bd93e06334ba090b12b6aff2f6fcb8ff9613e3c9bc48f48"},
{file = "opencv_python-4.5.1.48-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c0503bfaa2b7b743d6ff5d81f1dd8428dbf4c33e7e4f836456d11be20c2e7721"},
{file = "opencv_python-4.5.1.48-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:e27d062fa1098d90f48b6c047351c89816492a08906a021c973ce510b04a7b9d"},
{file = "opencv_python-4.5.1.48-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:6d8434a45e8f75c4da5fd0068ce001f4f8e35771cc851d746d4721eeaf517e25"},
{file = "opencv_python-4.5.1.48-cp37-cp37m-win32.whl", hash = "sha256:e2c17714da59d9d516ceef0450766ff9557ee232d62f702665af905193557582"},
{file = "opencv_python-4.5.1.48-cp37-cp37m-win_amd64.whl", hash = "sha256:efac9893d9e21cfb599828801c755ecde8f1e657f05ec6f002efe19422456d5a"},
{file = "opencv_python-4.5.1.48-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:e77d0feaff37326f62b127098264e2a7099deb476e38432b1083ce11cdedf560"},
{file = "opencv_python-4.5.1.48-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ffc75c614b8dc3d8102f3ba15dafd6ec0400c7ffa71a91953d41511964ee50e0"},
{file = "opencv_python-4.5.1.48-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c1159d91f29a85c3333edef6ca420284566d9bcdae46dda2fe7282515b48c8b6"},
{file = "opencv_python-4.5.1.48-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:d16144c435b816c5536d5ff012c1a2b7e93155017db7103942ff7efb98c4df1f"},
{file = "opencv_python-4.5.1.48-cp38-cp38-win32.whl", hash = "sha256:b2b9ac86aec5f2dd531545cebdea1a1ef4f81ef1fb1760d78b4725f9575504f9"},
{file = "opencv_python-4.5.1.48-cp38-cp38-win_amd64.whl", hash = "sha256:30edebc81b260bcfeb760b3600c367c5261dfb2fe41e5d1408d5357d0867b40d"},
{file = "opencv_python-4.5.1.48-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:e38fbd7b2db03204ec09930609b7313d6b6d2b271c8fe2c0aa271fa69b726a1b"},
{file = "opencv_python-4.5.1.48-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:fc1472b825d26c8a4f1cfb172a90c3cc47733e4af7522276c1c2efe8f6006a8b"},
{file = "opencv_python-4.5.1.48-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c4ea4f8b217f3e8be6247fc0787fb81797d85202c722523f41070124a7a621c7"},
{file = "opencv_python-4.5.1.48-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:a1dfa0486db367594510c0c799ec7481247dc86e651b69008806d875ab731471"},
{file = "opencv_python-4.5.1.48-cp39-cp39-win32.whl", hash = "sha256:5172cb37dfd8a0b4945b071a493eb36e5f17675a160637fa380f9c1d9d80535c"},
{file = "opencv_python-4.5.1.48-cp39-cp39-win_amd64.whl", hash = "sha256:c8cc1f5ff3c352ebe756119014c4e4ec7ae5ac536d1f66b0316667ced37637c8"},
]
opencv-python-headless = [
{file = "opencv-python-headless-4.5.1.48.tar.gz", hash = "sha256:d16825755e7b5a6d8737f93e116670229e1510199e0af9213004e187ae0dbcc5"},
{file = "opencv_python_headless-4.5.1.48-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:f2011ecb3980bbed283d17d43e0f1221bac88c0cac1a6fb59a056544de2df2f7"},
@@ -481,40 +595,42 @@ pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
]
pefile = [
{file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"},
]
pillow = [
{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"},
{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"},
]
psutil = [
{file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
@@ -554,8 +670,8 @@ pyinstaller = [
{file = "pyinstaller-4.2.tar.gz", hash = "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c"},
]
pyinstaller-hooks-contrib = [
{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"},
{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"},
]
pyobjc-core = [
{file = "pyobjc-core-7.1.tar.gz", hash = "sha256:a0616d5d816b4471f8f782c3a9a8923d2cc85014d88ad4f7fec694be9e6ea349"},
@@ -601,6 +717,10 @@ pyside2 = [
{file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win32.whl", hash = "sha256:087a0b719bb967405ea85fd202757c761f1fc73d0e2397bc3a6a15376782ee75"},
{file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff"},
]
pywin32-ctypes = [
{file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
{file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
]
regex = [
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
+8 -3
View File
@@ -4,7 +4,6 @@ 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"
@@ -18,11 +17,17 @@ pyobjc-framework-ApplicationServices = [{ version="7.1", platform="darwin" }]
pyobjc-framework-CoreText = [{ version="7.1", platform="darwin" }]
click = "^7.1.2"
psutil = "^5.8.0"
opencv-python = "^4.5.1"
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.scripts]
runekit = 'runekit.main:main'
[tool.poetry.build]
script = "build.py"
[build-system]
requires = ["poetry-core>=1.1.0"]
build-backend = "poetry.core.masonry.api"
File renamed without changes.
-10
View File
@@ -25,13 +25,3 @@ class RequestHandler(typing.TypedDict):
handlerName: str
handlerUrl: str
handlerScript: str
class RectLike(typing.TypedDict):
x: int
y: int
width: int
height: int
CaptureMulti = typing.Dict[str, typing.Optional[RectLike]]
+1 -1
View File
@@ -5,7 +5,7 @@ import requests
from runekit.alt1.schema import AppManifest
def fetch_bom_json(url: str):
def fetch_manifest(url: str) -> AppManifest:
req = requests.get(url)
req.raise_for_status()
-1
View File
@@ -1,2 +1 @@
from .app import App
from .store import AppStore
+1 -20
View File
@@ -13,34 +13,18 @@ if TYPE_CHECKING:
class App:
host: "Host"
manifest: AppManifest
app_id: str
game_instance: "GameInstance"
source_url: Optional[str]
window: Optional[AppWindow] = None
alt1api: Optional[Alt1Api] = None
web_profile: Optional[WebProfile] = None
def __init__(self, host, app_id, manifest, game_instance, source_url=None):
def __init__(self, host, manifest, game_instance, source_url=None):
self.host = host
self.app_id = app_id
self.manifest = manifest
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
@@ -67,7 +51,6 @@ class App:
@property
def absolute_app_url(self) -> str:
# TODO: Remove
if self.source_url:
return urljoin(self.source_url, self.manifest["appUrl"])
@@ -75,7 +58,6 @@ class App:
@property
def absolute_config_url(self) -> str:
# TODO: Remove
if self.source_url:
return urljoin(self.source_url, self.manifest["configUrl"])
@@ -83,7 +65,6 @@ class App:
@property
def absolute_icon_url(self) -> Optional[str]:
# TODO: Remove
if self.source_url and "iconUrl" in self.manifest:
return urljoin(self.source_url, self.manifest["iconUrl"])
-340
View File
@@ -1,340 +0,0 @@
import sys
import traceback
from pathlib import Path
import json
import logging
import hashlib
from typing import Iterator, Tuple, Optional, Union, List
from urllib.parse import urljoin
import requests
from PySide2.QtCore import (
QObject,
QSettings,
QThread,
Signal,
Slot,
QStandardPaths,
)
from PySide2.QtGui import QIcon, QPixmap
from PySide2.QtWidgets import QProgressDialog, QMessageBox
from runekit.alt1.schema import AppManifest
from runekit.alt1.utils import fetch_bom_json
REGISTRY_URL = "https://runeapps.org/data/alt1/defaultapps.json"
logger = logging.getLogger(__name__)
def app_id(manifest_url):
assert "://" in manifest_url
return hashlib.blake2b(manifest_url.encode("utf8"), digest_size=16).hexdigest()
class _FetchRegistryThread(QThread):
progress = Signal(int)
items = Signal(int)
label = Signal(str)
failed = Signal(tuple)
canceled = False
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:
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
finally:
self.items.emit(100)
self.progress.emit(100)
def add_app(self, url, folder=""):
try:
manifest = fetch_bom_json(url)
self.appstore.add_app(url, manifest)
self.appstore.add_app_to_folder(app_id(url), folder)
except:
logger.warning("Unable to add application %s", url, exc_info=True)
self.failed.emit(sys.exc_info())
@Slot()
def cancel(self):
self.canceled = True
class AppStore(QObject):
app_change = Signal()
def __init__(self):
super().__init__()
self.settings = QSettings(self)
qt_write_base = Path(
QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)
)
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)
def has_default_apps(self) -> bool:
return self.settings.value("apps/_meta/isDefaultLoaded")
def load_default_apps(self):
def on_progress(value):
if value != self.app_progress.maximum():
return
self.settings.setValue("apps/_meta/isDefaultLoaded", True)
logger.info("Default apps loaded")
self.add_app_thread.progress.disconnect(on_progress)
del self.app_progress
del self.add_app_thread
self.app_progress = QProgressDialog("Loading default apps", "Cancel", 0, 100)
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.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)
try:
manifest["appUrl"] = urljoin(manifest_url, manifest["appUrl"])
manifest["iconUrl"] = (
urljoin(manifest_url, manifest["iconUrl"])
if "iconUrl" in manifest and manifest["iconUrl"]
else ""
)
manifest["configUrl"] = urljoin(manifest_url, manifest["configUrl"])
except KeyError:
raise AddAppError(manifest_url)
if manifest["iconUrl"]:
self.download_app_icon(appid, manifest["iconUrl"])
self.settings.setValue(f"apps/{appid}", json.dumps(manifest))
self.app_change.emit()
logger.info(
"Application %s (%s:%s) installed", manifest["appName"], appid, manifest_url
)
def mkdir(self, folder: str):
assert folder
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
settings.setValue("_dir", "true")
self.app_change.emit()
def add_app_to_folder(self, appid: str, folder: str = "", _emit=True):
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
last_id = 0
for key in settings.childKeys():
if "_" in key:
continue
last_id = max(int(key), last_id)
if appid == settings.value(key):
settings.endGroup()
return
keys = [int(x) for x in settings.childKeys() if "_" not in x]
last_id = max(keys) if keys else 0
settings.endGroup()
settings.setValue(f"apps/_folder/{folder}/{last_id + 1}", appid)
if _emit:
self.app_change.emit()
def delete_app_from_folder(self, appid: str, folder: str):
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
for key in settings.childKeys():
if appid == settings.value(key):
settings.remove(key)
self.app_change.emit()
return
def remove_app(self, appid: str):
settings = QSettings()
settings.beginGroup("apps/_folder/")
def recurse():
for key in settings.childKeys():
if settings.value(key) == appid:
settings.remove(key)
for childGroup in settings.childGroups():
settings.beginGroup(childGroup)
recurse()
settings.endGroup()
recurse()
self.settings.remove(f"apps/{appid}")
icon_file = self.icon_write_dir / (appid + ".png")
icon_file.unlink(True)
self.app_change.emit()
def rmdir(self, folder: str):
assert folder
assert "/" not in folder
settings = QSettings()
settings.beginGroup(f"apps/_folder/{folder}")
for key in settings.childKeys():
if "_" in key:
continue
try:
int(key)
except ValueError:
continue
self.add_app_to_folder(settings.value(key), "", _emit=False)
settings.remove("")
self.app_change.emit()
def download_app_icon(self, appid: str, url: str):
dest = self.icon_write_dir / (appid + ".png")
req = requests.get(url)
with dest.open("wb") as fp:
fp.write(req.content)
logger.info("App icon %s wrote to %s", appid, str(dest))
def icon(self, appid: str) -> Optional[QIcon]:
fn = QStandardPaths.locate(
QStandardPaths.AppConfigLocation, "app_icons/" + appid + ".png"
)
if fn == "":
return None
return QIcon(QPixmap(fn))
def all_apps(self) -> Iterator[Tuple[str, AppManifest]]:
settings = QSettings()
settings.beginGroup("apps")
for appid in settings.childKeys():
if appid.startswith("_"):
continue
manifest = json.loads(settings.value(appid))
yield appid, manifest
settings.endGroup()
def list_app(self, root: str) -> Iterator[Tuple[str, Union[AppManifest, None]]]:
settings = QSettings()
settings.beginGroup(f"apps/_folder/" + root)
for key in settings.childGroups():
yield key, None
for key in settings.childKeys():
try:
int(key)
except ValueError:
continue
appid = settings.value(key)
yield appid, self[appid]
def __iter__(self):
yield from self.all_apps()
def __getitem__(self, item: str) -> AppManifest:
assert not item.startswith("_")
return json.loads(self.settings.value(f"apps/{item}"))
class AddAppError(ValueError):
def __init__(self, url):
super().__init__(f"Unable to parse manifest of application at {url}")
+2 -5
View File
@@ -1,5 +1,4 @@
import logging
import sys
from typing import TYPE_CHECKING
from PySide2.QtCore import Qt, Slot, QRect, QObject, QUrl
@@ -8,7 +7,6 @@ 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:
@@ -57,7 +55,7 @@ class PageClass(QWebEnginePage):
return super().acceptNavigationRequest(url, type_, is_main_frame)
class BrowserWindow(GameSnapMixin, QMainWindow):
class BrowserWindow(QMainWindow):
app: "App"
browser: QWebEngineView
page_class = PageClass
@@ -67,8 +65,7 @@ class BrowserWindow(GameSnapMixin, QMainWindow):
super().__init__(**kwargs)
self.app = app
if self.framed and sys.platform != "darwin":
self.setAttribute(Qt.WA_TranslucentBackground)
if self.framed:
self.frame = WindowFrame(parent=self)
self.frame.on_exit.connect(self.close)
self.setCentralWidget(self.frame)
+39 -15
View File
@@ -2,8 +2,9 @@ import logging
import sys
from typing import Optional
from PySide2.QtCore import QSize, Qt, QThreadPool, QSettings
from PySide2.QtGui import QIcon
import requests
from PySide2.QtCore import QSize, Qt, QRunnable, Slot, QObject, Signal, QThreadPool
from PySide2.QtGui import QIcon, QPixmap
from runekit.app.view.browser_window import BrowserWindow
@@ -13,20 +14,18 @@ class AppWindow(BrowserWindow):
logger: logging.Logger
app_icon: Optional[QIcon]
def __init__(self, **kwargs):
self.settings = QSettings()
super().__init__(**kwargs)
self.settings.setParent(self)
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 | Qt.Window
flags = Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint
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__
@@ -37,14 +36,20 @@ class AppWindow(BrowserWindow):
)
self.browser.load(self.app.absolute_app_url)
self._update_app_icon()
self.app_icon = self.app.host.app_store.icon(self.app.app_id)
if self.app_icon:
self.setWindowIcon(self.app_icon)
def _update_app_icon(self):
if self.app.absolute_icon_url:
self.logger.debug("Fetching app icon")
icon_job = IconFetcher(self.app.absolute_icon_url)
@property
def framed(self) -> bool:
return self.settings.value("settings/styledBorder", "true") == "true"
def update_app_icon(icon: QPixmap):
self.logger.debug("App icon loaded")
self.app_icon = icon
self.setWindowIcon(QIcon(icon))
icon_job.signals.finished.connect(update_app_icon)
self.pool.start(icon_job)
def minimumSize(self) -> QSize:
return QSize(self.app.manifest["minWidth"], self.app.manifest["minHeight"])
@@ -56,3 +61,22 @@ class AppWindow(BrowserWindow):
def maximumSize(self) -> QSize:
return QSize(self.app.manifest["maxWidth"], self.app.manifest["maxHeight"])
class IconFetcherSignal(QObject):
finished = Signal(QPixmap)
class IconFetcher(QRunnable):
def __init__(self, icon_url: str):
super().__init__()
self.icon_url = icon_url
self.signals = IconFetcherSignal()
@Slot()
def run(self):
icon_req = requests.get(self.icon_url)
icon_req.raise_for_status()
pixmap = QPixmap()
pixmap.loadFromData(icon_req.content)
self.signals.finished.emit(pixmap)
+1 -52
View File
@@ -40,19 +40,9 @@
}
});
function str2ab(str) {
let buf = new ArrayBuffer(str.length);
let bufView = new Uint8ClampedArray(buf);
for (let i=0, strLen=str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i) & 0xFF;
}
return bufView;
}
function syncRpc(msg, json) {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'rk:' + JSON.stringify(msg), false); // sync xhr
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.setRequestHeader('token', RPC_TOKEN);
xhr.send(null);
@@ -62,25 +52,6 @@
return xhr.responseText;
}
function asyncRpc(msg, responseType='') {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'rk:' + JSON.stringify(msg));
xhr.setRequestHeader('token', RPC_TOKEN);
xhr.responseType = responseType;
xhr.addEventListener('load', (e) => {
resolve(xhr.response);
});
xhr.addEventListener('error' ,(e) => {
e.xhr = xhr;
reject(e);
});
xhr.send();
return xhr.response;
})
}
function emit(param){
let listeners = alt1.events[param.eventName];
for(let i = 0; i < listeners.length; i++){
@@ -350,30 +321,8 @@
// return -1;
// },
// bindFindSubImg(id, imgstr, imgwidth, x, y, w, h) {
// return '';
// return syncRpc({func: 'bindFindSubImg', id: id, subimg: imgstr, img_width: imgwidth, x: x, y: y, w: w, h: h});
// },
capture(x, y, w, h) {
let data = syncRpc({func: 'getRegionRaw', x: x, y: y, w: w, h: h});
return str2ab(data);
},
captureAsync(x, y, w, h) {
return asyncRpc({func: 'getRegionRaw', x: x, y: y, w: w, h: h}, 'arraybuffer')
.then((data) => new Uint8ClampedArray(data));
},
async captureMultiAsync(areas) {
// XXX: This override the current bind
let boundId = await asyncRpc({func: 'bindRegion', x: 0, y: 0, w: alt1.rsWidth, h: alt1.rsHeight}, 'json');
let bounds = await Promise.all(Object.keys(areas).map((area) => {
let value = areas[area];
return asyncRpc({func: 'bindGetRegionRaw', id: boundId, x: value.x, y: value.y, w: value.width, h: value.height}, 'arraybuffer')
.then((data) => ({[area]: new Uint8ClampedArray(data)}));
}));
return bounds.reduce((a, b) => ({...a, ...b}));
},
bindGetRegionBuffer(id, x, y, w, h) {
let data = syncRpc({func: 'bindGetRegionRaw', id: id, x: x, y: y, w: w, h: h});
return str2ab(data);
},
addOCRFont(){
},
};
+22 -20
View File
@@ -2,6 +2,7 @@ import base64
import json
import logging
import secrets
import time
from typing import TYPE_CHECKING, Dict, Callable, List, NamedTuple
from urllib.parse import urljoin
@@ -30,6 +31,8 @@ from runekit.browser.utils import (
decode_image,
)
from runekit.game.instance import ImageType
from runekit.image.cv import find_subimages
from runekit.image.np_utils import ensure_np_image, np_crop
from runekit.ui.tray import tray_icon
if TYPE_CHECKING:
@@ -68,10 +71,9 @@ class Alt1Api(QObject):
self.rpc_funcs = {
"getRegion": self.get_region,
"getRegionRaw": self.get_region_raw,
"bindRegion": self.bind_region,
"bindGetRegion": self.bind_get_region,
"bindGetRegionRaw": self.bind_get_region_raw,
"bindFindSubImg": self.bind_find_subimage,
}
self._update_screen_info()
@@ -205,17 +207,7 @@ class Alt1Api(QObject):
if not self.app.has_permission("pixel"):
raise ApiPermissionDeniedException("pixel")
return base64.b64encode(
image_to_stream(self.app.game_instance.grab_region(x, y, w, h))
)
def get_region_raw(self, x, y, w, h):
if not self.app.has_permission("pixel"):
raise ApiPermissionDeniedException("pixel")
return image_to_stream(
self.app.game_instance.grab_region(x, y, w, h), mode="rgba"
)
return image_to_stream(self.app.game_instance.grab_region(x, y, w, h))
def bind_region(self, x, y, w, h):
if not self.app.has_permission("pixel"):
@@ -239,22 +231,32 @@ class Alt1Api(QObject):
self.logger.warning("bindGetRegion(%d) but image not bound", id)
return ""
return base64.b64encode(image_to_stream(image.image, x, y, w, h))
def bind_get_region_raw(self, id, x, y, w, h):
if not self.app.has_permission("pixel"):
raise ApiPermissionDeniedException("pixel")
return image_to_stream(image.image, x, y, w, h)
def bind_find_subimage(
self, id: int, subimg: str, img_width: int, x: int, y: int, w: int, h: int
):
if id == 0:
return ""
assert x >= 0
assert y >= 0
try:
image = self._bound_regions[id - 1]
except IndexError:
self.logger.warning("bindGetRegionRaw(%d) but image not bound", id)
self.logger.warning("bindGetRegion(%d) but image not bound", id)
return ""
return image_to_stream(image.image, x, y, w, h, mode="rgba")
image = ensure_np_image(image.image)
image = np_crop(image, x, y, w, h)
subimg = decode_image(subimg, img_width)
start = time.monotonic()
out = find_subimages(image, subimg)
self.logger.debug("OCR took %fs", time.monotonic() - start)
out = [{"x": int(i[0]), "y": int(i[1])} for i in out]
return json.dumps(out)
# endregion
+9 -51
View File
@@ -1,11 +1,10 @@
import base64
from typing import TypeVar
from typing import Union
import numpy as np
from PIL import Image
from PySide2.QtGui import QColor
from runekit.game.instance import ImageType
from runekit.image.np_utils import np_crop
TRANSFER_LIMIT = 4_000_000
@@ -21,56 +20,13 @@ class ApiPermissionDeniedException(Exception):
self.required_permission = required_permission
ImgTypeG = TypeVar("T", np.ndarray, Image.Image)
def ensure_image_rgba(image: ImgTypeG) -> ImgTypeG:
# XXX: This function is not idempotent!
if isinstance(image, np.ndarray):
return image[:, :, [2, 1, 0, 3]]
else:
if image.mode == "RGB":
image = image.convert("RGBA")
return image
def ensure_image_bgra(image: ImgTypeG) -> ImgTypeG:
# XXX: This function is not idempotent!
if isinstance(image, np.ndarray):
return image
else:
if image.mode == "RGB":
image = image.convert("RGBA")
r, g, b, a = image.split()
image = Image.merge("RGBA", (b, g, r, a))
return image
def ensure_image(image: ImgTypeG, mode: str) -> ImgTypeG:
if mode == "rgba":
return ensure_image_rgba(image)
elif mode == "bgra":
return ensure_image_bgra(image)
else:
raise ValueError("invalid mode")
def image_to_stream(
image: ImageType,
x=0,
y=0,
width=None,
height=None,
mode="bgra",
image: Union[Image.Image, np.ndarray], x=0, y=0, width=None, height=None
) -> bytes:
if isinstance(image, np.ndarray):
out = np_crop(image, x, y, width, height)
out = ensure_image(out, mode).tobytes()
return base64.b64encode(np_crop(image, x, y, width, height).tobytes())
else:
assert image.mode == "RGBA"
assert image.mode == "RGB" or image.mode == "RGBA"
if width is None:
width = image.width
@@ -80,11 +36,13 @@ def image_to_stream(
if width * height * 4 > TRANSFER_LIMIT:
return ""
image = image.crop((x, y, x + width, y + height))
if image.mode == "RGB":
image = image.convert("RGBA")
out = ensure_image(image, mode).tobytes()
r, g, b, a = image.crop((x, y, x + width, y + height)).split()
image = Image.merge("RGBA", (b, g, r, a))
return out
return base64.b64encode(image.tobytes())
def encode_mouse(x: int, y: int) -> int:
+2 -2
View File
@@ -8,7 +8,7 @@ from PySide2.QtCore import QObject, Signal, Property, QRect, Slot
from PySide2.QtGui import QWindow
from PySide2.QtWidgets import QGraphicsItem
from runekit.image.np_utils import np_crop
from ..image.np_utils import np_crop
if TYPE_CHECKING:
from .manager import GameManager
@@ -68,7 +68,7 @@ class GameInstance(QObject):
world = Property(int, get_world, notify=worldChanged)
@abc.abstractmethod
def grab_game(self) -> ImageType:
def grab_game(self) -> Union[Image.Image, np.ndarray]:
...
@abc.abstractmethod
+3 -7
View File
@@ -1,5 +1,5 @@
import abc
from typing import List, Union
from typing import List
from PySide2.QtCore import QObject, Property, Signal
@@ -12,12 +12,8 @@ class GameManager(QObject):
"""Return a list of active game instance. The instances returned should be stable (same instance for all invocation)"""
...
@abc.abstractmethod
def get_active_instance(self) -> Union[GameInstance, None]:
...
instance_added = Signal(GameInstance)
instance_removed = Signal(GameInstance)
instance_added = Signal()
instance_removed = Signal()
instance_changed = Signal()
instances = Property(list, get_instances, notify=instance_changed)
+1 -26
View File
@@ -1,8 +1,6 @@
import logging
from typing import TYPE_CHECKING, Callable, Tuple, Dict
import numpy as np
from PySide2.QtCore import Qt, QRect, QTimer
from PySide2.QtCore import Qt, QRect
from PySide2.QtGui import QGuiApplication, QPen
from PySide2.QtWidgets import (
QMainWindow,
@@ -12,8 +10,6 @@ from PySide2.QtWidgets import (
QGraphicsRectItem,
)
from .qt import qpixmap_to_np
if TYPE_CHECKING:
from .instance import GameInstance
@@ -31,11 +27,9 @@ 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)
@@ -94,22 +88,3 @@ 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
View File
@@ -12,7 +12,7 @@ _debug_dump_file = False
logger = logging.getLogger(__name__)
def qpixmap_to_np(im: QPixmap) -> np.ndarray:
def qpixmap_to_np(im: QPixmap):
# from PIL.ImageQt.fromqimage
buffer = QBuffer()
buffer.open(QIODevice.ReadWrite)
+1 -1
View File
@@ -12,7 +12,7 @@ import ApplicationServices
from PySide2.QtWidgets import QGraphicsItem
from ..instance import GameInstance
from ..psutil_mixins import PsUtilNetStat
from ..psutil import PsUtilNetStat
if TYPE_CHECKING:
from .manager import QuartzGameManager
+2 -13
View File
@@ -1,6 +1,6 @@
import time
from functools import reduce
from typing import List, Dict, Optional, Union
from typing import List, Dict, Optional
import Quartz
import ApplicationServices
@@ -61,14 +61,9 @@ 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, start)
QTimer.singleShot(1000, lambda: self.overlay.show())
def stop(self):
try:
@@ -93,12 +88,6 @@ 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:
+5 -12
View File
@@ -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_mixins import PsUtilNetStat
from runekit.game.psutil import PsUtilNetStat
from runekit.game.qt import QtGrabMixin, QtEmbedMixin
from .ximage import zpixmap_shm_to_image
@@ -31,7 +31,6 @@ 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)
@@ -54,7 +53,7 @@ class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
self.wid, xcffib.composite.Redirect.Automatic
)
self.name_pixmap()
self.manager.connection.core.ChangeWindowAttributesChecked(
self.manager.connection.core.ChangeWindowAttributes(
self.wid,
xcffib.xproto.CW.EventMask,
[
@@ -97,20 +96,14 @@ class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
return self.cached_position
def get_scaling(self) -> float:
pos = self.get_position().topLeft()
if pos.x() < 0:
pos.setX(0)
if pos.y() < 0:
pos.setY(0)
screen = QGuiApplication.screenAt(pos)
screen = QGuiApplication.screenAt(self.get_position().topLeft())
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
+22 -95
View File
@@ -1,5 +1,4 @@
import logging
import os
import struct
from typing import List, Dict, Tuple, Union
@@ -9,7 +8,7 @@ import xcffib.composite
import xcffib.shm
import xcffib.xinput
import xcffib.xproto
from PySide2.QtCore import QThread, Slot, QObject, Signal
from PySide2.QtCore import QThread, Slot, QObject
from runekit.game import GameManager
from .instance import GameInstance, X11GameInstance
@@ -17,7 +16,6 @@ from ..overlay import DesktopWideOverlay
MAX_SHM = 10
NET_ACTIVE_WINDOW = "_NET_ACTIVE_WINDOW"
WM_APP_NAME = os.getenv("RK_WM_APP_NAME", "RuneScape")
class X11GameManager(GameManager):
@@ -33,7 +31,6 @@ 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)
@@ -48,13 +45,9 @@ 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()
@@ -62,56 +55,29 @@ class X11GameManager(GameManager):
def get_instances(self) -> List[GameInstance]:
def visit(wid: int):
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
wm_class = self.get_property(wid, xcffib.xproto.Atom.WM_CLASS)
try:
query = self.connection.core.QueryTree(wid).reply()
for child in query.children:
visit(child)
except xcffib.xproto.WindowError:
return
if wm_class:
instance_name, app_name = wm_class.split("\00")
if app_name == "RuneScape":
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)
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)
@@ -179,27 +145,8 @@ 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
@@ -210,21 +157,17 @@ 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.ChangeWindowAttributesChecked(
self.manager.connection.core.ChangeWindowAttributes(
self.manager.screen.root,
xcffib.xproto.CW.EventMask,
[
xcffib.xproto.EventMask.PropertyChange
| xcffib.xproto.EventMask.SubstructureNotify,
],
).check()
[xcffib.xproto.EventMask.PropertyChange],
is_checked=True,
)
current_thread = QThread.currentThread()
while True:
@@ -246,11 +189,7 @@ class X11EventWorker(QObject):
)
def on_property_change(self, evt: xcffib.xproto.PropertyNotifyEvent):
if (
evt.state == xcffib.xproto.Property.NewValue
and evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW)
and evt.window == self.manager.screen.root
):
if evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW):
active_win_id = self.manager.get_active_window()
if self.active_win_id == active_win_id:
@@ -261,8 +200,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(
@@ -277,18 +216,6 @@ class X11EventWorker(QObject):
try:
self.manager._instances[evt.window].config_signal.emit(evt)
except KeyError:
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)
self.logger.debug(
"Got configure event for %d but is not registered", evt.window
)
+43
View File
@@ -0,0 +1,43 @@
from PySide2.QtCore import QAbstractItemModel, Qt, QModelIndex
class ApplicationModel(QAbstractItemModel):
h_columns = ("Name", "URL", "Description")
def columnCount(self, parent: QModelIndex) -> int:
return len(self.h_columns)
def rowCount(self, parent: QModelIndex) -> int:
if parent.isValid():
return 0
return 1
def data(self, index: QModelIndex, role: int):
if role != Qt.DisplayRole:
return None
col = index.column()
if col == 0:
return "TestApp"
elif col == 1:
return "https://runeapps.org/apps/alt1/example/appconfig.json"
elif col == 2:
return "Lorem ipsum dolor sit amet"
def index(self, row: int, column: int, parent: QModelIndex) -> QModelIndex:
return self.createIndex(row, column)
def parent(self, index: QModelIndex) -> QModelIndex:
return QModelIndex()
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role != Qt.DisplayRole:
return None
if orientation != Qt.Orientation.Horizontal:
return None
return self.h_columns[section]
-194
View File
@@ -1,194 +0,0 @@
import typing
import json
from typing import Optional, TypedDict, Union, List
from PySide2.QtCore import QAbstractItemModel, Qt, QModelIndex, QMimeData
from PySide2.QtGui import QIcon
from runekit.alt1.schema import AppManifest
from runekit.app import AppStore
MIMETYPE = "application/x-qabstractitemmodeldatalist"
class _InternalData(TypedDict):
id: str
data: Optional[AppManifest]
index: QModelIndex
parent: Union[QModelIndex, None]
children: List["_InternalData"]
class AppStoreModel(QAbstractItemModel):
h_columns = ("Name", "URL", "Description")
model: List[_InternalData]
def __init__(self, parent, store: AppStore):
super().__init__(parent)
self.store = store
self.store.app_change.connect(self.on_app_change)
self.model = self.build_model()
def on_app_change(self):
self.beginResetModel()
self.model = self.build_model()
self.endResetModel()
def build_model(self, root="", parent=None) -> List[_InternalData]:
apps = self.store.list_app(root)
out = []
for app_id, meta in apps:
if not meta:
data = {
"id": app_id,
"parent": parent,
}
index = data["index"] = self.createIndex(len(out), 0, data)
data["children"] = self.build_model(app_id, index)
out.append(data)
continue
data = {
"id": app_id,
"data": meta,
"parent": parent,
}
data["index"] = self.createIndex(len(out), 0, data)
out.append(data)
return out
def columnCount(self, parent: QModelIndex) -> int:
return len(self.h_columns)
def rowCount(self, parent: QModelIndex) -> int:
if parent.isValid():
data = typing.cast(_InternalData, parent.internalPointer())
return len(data["children"])
return len(self.model)
def data(self, index: QModelIndex, role: int):
left_col = index.siblingAtColumn(0)
col = index.column()
app = typing.cast(_InternalData, left_col.internalPointer())
if "data" not in app:
# Folder
if role == Qt.DisplayRole:
if col == 0:
return app["id"]
elif role == Qt.DecorationRole:
if col == 0:
return QIcon.fromTheme("folder")
return None
if role in (Qt.DisplayRole, Qt.ToolTipRole):
if col == 0:
return app["data"]["appName"]
elif col == 1:
return app["data"]["appUrl"]
elif col == 2:
return app["data"]["description"]
elif role == Qt.DecorationRole and col == 0:
return self.store.icon(app["id"])
else:
return None
def hasChildren(self, index: QModelIndex) -> bool:
if not index.isValid():
return True
if index.column() != 0 or not index.internalPointer():
return False
data = typing.cast(_InternalData, index.internalPointer())
return "data" not in data
def index(self, row: int, column: int, parent: QModelIndex) -> QModelIndex:
if column != 0:
leftmost = self.index(row, 0, parent)
return self.createIndex(row, column, leftmost.internalPointer())
if parent.isValid():
data = typing.cast(_InternalData, parent.internalPointer())
return data["children"][row]["index"]
if row >= len(self.model):
return QModelIndex()
return self.model[row]["index"]
def parent(self, index: QModelIndex) -> QModelIndex:
data = typing.cast(_InternalData, index.internalPointer())
return data["parent"] or QModelIndex()
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
out = Qt.ItemIsSelectable | Qt.ItemIsEnabled
if not index.isValid():
return out | Qt.ItemIsDropEnabled
data = typing.cast(_InternalData, index.internalPointer())
if "data" in data:
out |= Qt.ItemIsDragEnabled
else:
# Can't drag folder but can drop
out |= Qt.ItemIsDropEnabled
if "children" not in data:
out |= Qt.ItemNeverHasChildren
return out
def headerData(self, section: int, orientation: Qt.Orientation, role: int):
if role != Qt.DisplayRole:
return None
if orientation != Qt.Orientation.Horizontal:
return None
return self.h_columns[section]
def supportedDropActions(self) -> Qt.DropActions:
return Qt.MoveAction
def mimeData(self, indexes: List[QModelIndex]) -> QMimeData:
indexes = [item for item in indexes if item.column() == 0]
if not indexes:
return None
data = typing.cast(_InternalData, indexes[0].internalPointer())
out = QMimeData()
out_data = {
"id": data["id"],
"parent": "",
"type": "dir" if "data" not in data else "app",
}
if data["parent"]:
out_data["parent"] = data["parent"].internalPointer()["id"]
out.setData(MIMETYPE, json.dumps(out_data).encode("ascii"))
return out
def dropMimeData(
self,
data: QMimeData,
action: Qt.DropAction,
row: int,
column: int,
parent: QModelIndex,
) -> bool:
data = json.loads(data.data(MIMETYPE).data())
if parent.isValid():
new_folder = parent.internalPointer()["id"]
else:
new_folder = ""
if data["parent"] == new_folder:
return False
self.store.add_app_to_folder(data["id"], new_folder)
self.store.delete_app_from_folder(data["id"], data["parent"])
return True
+12 -94
View File
@@ -1,116 +1,34 @@
import logging
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING
from PySide2.QtCore import Slot, QCoreApplication
from PySide2.QtWidgets import QMessageBox
from runekit.alt1.schema import AppManifest
from runekit.alt1.utils import fetch_bom_json
from runekit.app import App, AppStore
from runekit.app.store import app_id
from runekit.alt1.utils import fetch_manifest
from runekit.app import App
from runekit.host.settings import SettingsDialog
from runekit.ui import AutoNotifier
from runekit.ui.tray import TrayIcon
if TYPE_CHECKING:
from runekit.game import GameInstance, GameManager
from runekit.game import GameInstance
class Host:
"""Host is the root of our application"""
open_app: List[App]
def __init__(self, manager: "GameManager"):
def __init__(self):
super().__init__()
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self.manager = manager
self.open_app = []
self.notifier = AutoNotifier()
self.app_store = AppStore()
self.tray_icon = TrayIcon(self)
self.setting_dialog = SettingsDialog(self)
self.tray_icon.show()
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:
app.close()
def __del__(self):
self.open_app = []
def launch_app_from_url(self, manifest_url) -> App:
manifest = fetch_bom_json(manifest_url)
manifest["configUrl"] = manifest_url
return self.launch_app(app_id(manifest_url), manifest)
@Slot(str)
def launch_app_id(self, appid):
try:
manifest = self.app_store[appid]
except KeyError:
QMessageBox.critical(
None,
"App missing",
"Cannot find application by that ID",
)
return
self.launch_app(appid, manifest)
def launch_app(self, appid: str, manifest: AppManifest):
instance = self.manager.get_active_instance()
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]
def start_app(self, manifest_url, instance: "GameInstance") -> App:
manifest = fetch_manifest(manifest_url)
app = App(
host=self,
app_id=appid,
manifest=manifest,
game_instance=instance,
source_url=manifest["configUrl"],
source_url=manifest_url,
)
def on_destroy():
try:
self.open_app.remove(app)
except ValueError:
pass
window = app.get_window()
window.show()
window.destroyed.connect(on_destroy)
self.open_app.append(app)
app.get_window().show()
return app
@Slot()
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)
wnd = SettingsDialog(self)
wnd.show()
return wnd
+29 -132
View File
@@ -1,9 +1,7 @@
import json
import sys
from typing import TYPE_CHECKING
from PySide2.QtCore import QSize, Qt, Slot, QTimer, QSettings
from PySide2.QtGui import QIcon
from PySide2.QtCore import QSize, Qt, Slot, QTimer
from PySide2.QtWidgets import (
QTabWidget,
QVBoxLayout,
@@ -17,14 +15,10 @@ from PySide2.QtWidgets import (
QHBoxLayout,
QTreeView,
QSpinBox,
QPushButton,
QAbstractItemView,
QInputDialog,
QMessageBox,
)
from .appstore_model import AppStoreModel
from ..ui import TooltipNotifier, TrayIconNotifier, AutoNotifier
from .applications import ApplicationModel
from ..ui import TooltipNotifier, TrayIconNotifier
if TYPE_CHECKING:
from .host import Host
@@ -33,6 +27,7 @@ if TYPE_CHECKING:
class SettingsDialog(QMainWindow):
def __init__(self, host: "Host", **kwargs):
super().__init__(flags=Qt.Dialog, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.host = host
self._layout()
self.setWindowTitle("RuneKit Settings")
@@ -46,7 +41,7 @@ class SettingsDialog(QMainWindow):
self.setContentsMargins(11, 11, 11, 11)
def sizeHint(self) -> QSize:
return QSize(800, 500)
return QSize(500, 300)
class ApplicationPage(QWidget):
@@ -55,97 +50,17 @@ class ApplicationPage(QWidget):
def __init__(self, host: "Host", **kwargs):
super().__init__(**kwargs)
self.host = host
self.model = AppStoreModel(self, self.host.app_store)
self._layout()
def _layout(self):
layout = QHBoxLayout(self)
self.setLayout(layout)
self.tree = QTreeView(self)
self.tree.setModel(self.model)
self.tree.setUniformRowHeights(True)
self.tree.setColumnWidth(0, 200)
self.tree.setDragEnabled(True)
self.tree.setDragDropMode(QAbstractItemView.InternalMove)
self.tree.viewport().setAcceptDrops(True)
layout.addWidget(self.tree, 1)
buttons = QVBoxLayout()
buttons.setAlignment(Qt.AlignTop)
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)
mkdir_button.setToolTip("Make directory")
mkdir_button.clicked.connect(self.on_mkdir)
buttons.addWidget(mkdir_button)
delete_button = QPushButton(QIcon.fromTheme("list-remove"), "", self)
delete_button.setToolTip("Remove selected item")
delete_button.clicked.connect(self.on_delete)
buttons.addWidget(delete_button)
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")
if dialog.exec_() == QInputDialog.Rejected:
return
dirname = dialog.textValue().strip()
if not dirname or "/" in dirname:
QMessageBox.critical(
self, "Invalid input", "This directory name cannot be used"
)
return
self.host.app_store.mkdir(dirname)
def on_delete(self):
data = self.model.mimeData(self.tree.selectedIndexes())
if not data:
return
data = json.loads(data.data("application/x-qabstractitemmodeldatalist").data())
if data["type"] == "dir":
confirm = QMessageBox.question(
self,
"Remove folder",
f"Remove {data['id']}?\n\nAll applications will be moved to the top level",
)
if confirm == QMessageBox.StandardButton.No:
return
self.host.app_store.rmdir(data["id"])
else:
app = self.host.app_store[data["id"]]
confirm = QMessageBox.question(
self,
"Remove application",
f"Remove {app['appName']}?",
)
if confirm == QMessageBox.StandardButton.No:
return
self.host.app_store.remove_app(data["id"])
# FIXME: Clear settings???
tree = QTreeView(self)
model = ApplicationModel(self)
tree.setModel(model)
tree.setUniformRowHeights(True)
layout.addWidget(tree, 1)
class InterfacePage(QWidget):
@@ -154,51 +69,43 @@ 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)
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.insertItem(0, "Disabled")
tooltip_field.insertItem(1, "Notification") # XXX: Check support
tooltip_field.insertItem(2, "Cursor tooltip")
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(
(self.settings.value("settings/styledBorder", "true") == "true")
and sys.platform != "darwin"
)
border_field.stateChanged.connect(self.on_change_styled_border)
border_field.setChecked(sys.platform != "darwin")
layout.addRow(None, border_field)
# I think this doesn't belong to "Interface"
# either rename it to settings or make separate game tab
refresh_label = QLabel("Refresh interval", self)
refresh_layout = QHBoxLayout(self)
refresh_field = QSpinBox(self)
refresh_field.setRange(100, 2000)
refresh_field.setSingleStep(100)
refresh_field.setValue(100)
refresh_layout.addWidget(refresh_field, 1)
refresh_unit = QLabel("ms", self)
refresh_layout.addWidget(refresh_unit, 0)
layout.addRow(refresh_label, refresh_layout)
@Slot(int)
def preview_tooltip(self, index: int):
type_ = list(AutoNotifier.availableMethods().items())[index][0]
if type_ == AutoNotifier.METHOD_NOTIFICATION:
if index == 1:
notifier = TrayIconNotifier()
elif type_ == AutoNotifier.METHOD_TOOLTIP:
elif index == 2:
notifier = TooltipNotifier()
else:
return
@@ -209,13 +116,3 @@ 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)
+59
View File
@@ -0,0 +1,59 @@
from typing import Tuple, List
import numpy as np
import cv2
_debug_save_images = False
def find_subimages(image: np.ndarray, subimage: np.ndarray) -> List[Tuple[int, int]]:
"""
Detect BGRA subimage in BGRA image with the following limitations
* Image transparency is not supported (alpha channel is removed)
* Subimage semi-translucency is not supported
"""
mask = subimage[:, :, 3]
image_bgr = image[:, :, :3]
subimage_bgr = subimage[:, :, :3]
matched = cv2.matchTemplate(image_bgr, subimage_bgr, cv2.TM_SQDIFF, mask=mask).get()
# allow up to 30 pixel per pixel
threshold = (30 ** 2) * subimage.shape[0] * subimage.shape[1]
loc = np.where((matched < threshold) & (matched != np.inf))
out = []
for pos in zip(*loc[::-1]):
out.append(pos)
if _debug_save_images:
from runekit.image.np_utils import np_save_image
import hashlib
cv2.normalize(matched, matched, 0, 0xFF, cv2.NORM_MINMAX)
matched = matched.astype("uint8")
image_bgr = image_bgr.copy()
subimg_hash = hashlib.blake2b(subimage.data, digest_size=8).hexdigest()
for pos in out:
cv2.rectangle(
matched,
pos,
(pos[0] + subimage.shape[1], pos[1] + subimage.shape[0]),
0,
2,
)
cv2.rectangle(
image_bgr,
pos,
(pos[0] + subimage.shape[1], pos[1] + subimage.shape[0]),
(0, 255, 0),
2,
)
np_save_image(image_bgr, f"/tmp/debug-image-{subimg_hash}.png")
np_save_image(subimage_bgr, f"/tmp/debug-subimage-{subimg_hash}.png")
np_save_image(mask, f"/tmp/debug-mask-{subimg_hash}.png")
np_save_image(matched, f"/tmp/debug-result-{subimg_hash}.png")
return out
-82
View File
@@ -1,82 +0,0 @@
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()
-41
View File
@@ -1,41 +0,0 @@
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
+3 -33
View File
@@ -1,7 +1,6 @@
import abc
from typing import Union, Dict
from typing import Union
from PySide2.QtCore import QObject, QSettings
from PySide2.QtWidgets import QSystemTrayIcon
from .tooltip import TooltipManager
@@ -14,11 +13,6 @@ class Notifier(abc.ABC):
...
class StubNotifier(Notifier):
def notify(self, text):
pass
class TooltipNotifier(Notifier):
def __init__(self):
self.manager = TooltipManager()
@@ -33,41 +27,17 @@ class TrayIconNotifier(Notifier):
tray_icon().showMessage("RuneKit", text)
class AutoNotifier(QObject):
class AutoNotifier(Notifier):
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()
self.notifier = TooltipNotifier()
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()
):
out[cls.METHOD_NOTIFICATION] = "Notification"
return out
View File
Whitespace-only changes.
+2 -56
View File
@@ -1,67 +1,13 @@
from typing import TYPE_CHECKING
from PySide2.QtCore import Slot, Signal, QCoreApplication
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import QSystemTrayIcon, QMenu
if TYPE_CHECKING:
from runekit.host import Host
from PySide2.QtWidgets import QSystemTrayIcon
class TrayIcon(QSystemTrayIcon):
on_settings = Signal()
host: "Host"
def __init__(self, host: "Host"):
def __init__(self):
global _tray_icon
super().__init__(QIcon(":/runekit/ui/trayicon.png"))
_tray_icon = self
self.host = host
self._setup_menu()
self.setContextMenu(self.menu)
def _setup_menu(self):
if not hasattr(self, "menu"):
self.menu = QMenu("RuneKit")
self.menu.clear()
self._setup_app_menu("", self.menu)
self.menu.addSeparator()
self.menu_settings = self.menu.addAction("Settings")
self.menu_settings.triggered.connect(self.on_settings)
self.menu.addAction(
QIcon.fromTheme("application-exit"),
"Exit",
lambda: QCoreApplication.instance().quit(),
)
def _setup_app_menu(self, path: str, menu: QMenu):
for app_id, manifest in self.host.app_store.list_app(path):
if manifest is None:
# Folder
submenu = menu.addMenu(QIcon.fromTheme("folder"), app_id)
self._setup_app_menu(app_id, submenu)
continue
app_menu = menu.addAction(manifest["appName"])
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:
app_menu.setIcon(icon)
@Slot()
def update_menu(self):
self._setup_menu()
def tray_icon() -> TrayIcon:
return _tray_icon
+7 -44
View File
@@ -1,4 +1,4 @@
from PySide2.QtCore import Signal, QPoint
from PySide2.QtCore import Signal
from PySide2.QtGui import QPixmap, Qt, QResizeEvent
from PySide2.QtWidgets import (
QWidget,
@@ -18,7 +18,6 @@ class WindowFrame(QWidget):
on_exit = Signal()
content: QWidget
shade = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -29,7 +28,6 @@ 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)
@@ -53,41 +51,6 @@ 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):
@@ -175,12 +138,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)