Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f40b130ad5 |
No files matched your search
@@ -1,53 +0,0 @@
|
||||
name: AppImage
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04 # Leave this on the earliest supported OS!
|
||||
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: 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,47 +0,0 @@
|
||||
name: Mac
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-11
|
||||
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: Install dependencies
|
||||
run: |
|
||||
python -m pip install poetry
|
||||
poetry install
|
||||
- name: Build resources
|
||||
run: poetry run make runekit/_resources.py
|
||||
- name: Build app
|
||||
run: poetry run make dist/RuneKit.app.zip
|
||||
|
||||
# Deploy
|
||||
- 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.app.zip
|
||||
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.app.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Bump tags
|
||||
on: [push]
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Bump tag
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: richardsimko/update-tag@v1
|
||||
with:
|
||||
tag_name: continuous
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Clear old releases
|
||||
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 }}
|
||||
@@ -1,10 +1,6 @@
|
||||
build/
|
||||
dist/
|
||||
runekit/image/format.c
|
||||
runekit/image/format.*.*
|
||||
runekit/image/*.c
|
||||
runekit/image/*.*.*
|
||||
setup.py
|
||||
*.egg-info
|
||||
__pycache__
|
||||
runekit/_resources.py
|
||||
*.AppImage
|
||||
*.pyo
|
||||
@@ -14,22 +14,6 @@ The `game` package is the only one allowed to use platform-dependent code. All o
|
||||
- game: Interact with the game window. Since accessing other application's window is not abstracted by Qt, this package is the only one allowed to be platform dependent
|
||||
- image: Image processing tools
|
||||
|
||||
### Image format
|
||||
The library has two internal image formats:
|
||||
|
||||
- numpy.ndarray of shape (height, width, 4)
|
||||
- Current standard
|
||||
- Store pixels in BGRA32 format (ARGB 8 bit per channel in little endian)
|
||||
- Should be faster as no channel swapping is needed
|
||||
- Requires copying when accessing raw buffer
|
||||
- Support OpenCV operation
|
||||
- PIL Image
|
||||
- Deprecated
|
||||
- Store pixels in RGBA32 format
|
||||
- Slower as when communicating with Alt1 we need to swap to BGRA
|
||||
- Can access raw buffer without copying
|
||||
- Easier to work with for basic operations
|
||||
|
||||
## App
|
||||
|
||||
Packages related to Alt1 app.
|
||||
@@ -41,17 +25,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
|
||||
@@ -1,47 +0,0 @@
|
||||
LINUXDEPLOY ?= linuxdeploy-$(shell uname -m).AppImage
|
||||
|
||||
dev: runekit/_resources.py
|
||||
|
||||
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/RuneKit.app: RuneKit.spec main.py poetry.lock runekit/_resources.py $(wildcard runekit/**/*)
|
||||
pyinstaller -w -n RuneKitApp --noconfirm \
|
||||
--exclude-module tkinter \
|
||||
-s -d noarchive \
|
||||
--osx-bundle-identifier de.cupco.runekit \
|
||||
$<
|
||||
|
||||
dist/RuneKit.app.zip: dist/RuneKit.app
|
||||
cd dist; zip -r -9 RuneKit.app.zip RuneKit.app
|
||||
|
||||
# AppImage
|
||||
|
||||
build/python3.9.7.AppImage:
|
||||
mkdir build || true
|
||||
wget https://github.com/niess/python-appimage/releases/download/python3.9/python3.9.7-cp39-cp39-manylinux1_x86_64.AppImage -O "$@"
|
||||
chmod +x "$@"
|
||||
|
||||
build/appdir: build/python3.9.7.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
|
||||
@@ -4,74 +4,74 @@
|
||||
|
||||
Alt1-compatible toolbox for RuneScape 3, for Linux and macOS.
|
||||
|
||||
**See also [Alt1 Electron](https://github.com/skillbert/alt1-electron) the experimental official Alt1 replacement from the Alt1 authors.**
|
||||
**Alpha quality software**
|
||||
|
||||
* [Compatibility](https://github.com/whs/runekit/wiki/App-Compatibility)
|
||||
* [Troubleshooting](https://github.com/whs/runekit/wiki/Troubleshooting)
|
||||
## What works
|
||||
|
||||
## Installing
|
||||
**Overlay are not supported yet**
|
||||
|
||||
### Linux
|
||||
App | Linux | macOS
|
||||
-------------------------------|-------|----------
|
||||
Clue solver (Text/Emote) | ✅ | ✅
|
||||
Clue solver (Map scroll) | ? | ?
|
||||
Clue solver (Compass) | ? | 2
|
||||
Clue solver (Scan) | ? | ?
|
||||
Clue solver (Lockbox) | ? | ?
|
||||
Clue solver (Celtic Knot) | ? | ✅
|
||||
Clue solver (Slide) | ? | ✅
|
||||
Clue solver (Towers) | ? | ?
|
||||
Example app | ✅ | ✅
|
||||
AfkWarden (Inactive) | ❌ | ✅
|
||||
AfkWarden (XP Counter) | ❌ | ❌
|
||||
AfkWarden (Chatbox) | 1 | ✅
|
||||
AfkWarden (Crafting menu) | ✅ | ✅
|
||||
AfkWarden (Buffs) | ✅ | ✅
|
||||
AfkWarden (Action bar stats) | ✅ | ✅
|
||||
AfkWarden (Dialog box) | ✅ | ✅
|
||||
AfkWarden (Target death) | ? | ?
|
||||
AfkWarden (Sheathe stance) | ? | ?
|
||||
AfkWarden (Castle Wars) | ? | ?
|
||||
AfkWarden (Fight kiln waves) | ? | ?
|
||||
AfkWarden (Target death) | ? | ?
|
||||
AfkWarden (Item drops) | ? | ?
|
||||
|
||||
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
|
||||
- 1: Can be setup but only fire once (needs inactive to work)
|
||||
- 2: Detected but cannot place mark
|
||||
|
||||
### macOS
|
||||
|
||||
1. [Download RuneKit.app](https://github.com/whs/runekit/releases/tag/continuous) and unzip (if you use Safari it should automatically unzip)
|
||||
2. Open Terminal (search in spotlight/launchpad if you can't find it)
|
||||
3. Type `xattr -dr com.apple.quarantine ` (including trailing space) and drop the app onto Terminal so it would be like `yourname@yourmacname ~ % xattr -dr com.apple.quarantine /Users/yourname/Downloads/RuneKit.app`. Press enter.
|
||||
4. Launch the app. The first launch might spring in the dock for a good minute.
|
||||
5. If permission prompt appears, grant it in System Preferences > Security. **Then quit RuneKit (right click dock icon > quit or force quit) and start it again.** Don't quit RuneKit while it is downloading app list! The list of permissions are:
|
||||
- Accessibility - for access to game window
|
||||
- Input Monitoring - for hooking alt+1 and idle detection
|
||||
- Screen Recording - for capturing game
|
||||
6. Once all permission has been granted the application appears as system tray icon (top right)
|
||||
|
||||
## 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.
|
||||
Requires Poetry 1.1
|
||||
|
||||
```sh
|
||||
poetry install
|
||||
# If previous fails and you're on Big Sur, try this instead
|
||||
SYSTEM_VERSION_COMPAT=1 poetry install
|
||||
|
||||
poetry run make dev
|
||||
|
||||
# If you just want to load AFKWarden
|
||||
poetry build
|
||||
cp build/lib.*/runekit/image/*.cpython*.* runekit/image/
|
||||
poetry run python main.py https://runeapps.org/apps/alt1/afkscape/appconfig.json
|
||||
# If you'd like to pick what app you load
|
||||
poetry run python main.py
|
||||
```
|
||||
|
||||
WIP Instruction:
|
||||
|
||||
- Run RuneScape before starting!!
|
||||
- Linux: Game window MUST be ENTIRELY visible. (No minimize, no other window on top, including RuneKit)
|
||||
- macOS: HiDPI (Retina) support may or may not work. Use external monitor for best result
|
||||
- macOS: You will need to add Python in System Preferences > Security > Privacy in these sections:
|
||||
- Accessibility
|
||||
- Screen Recording:
|
||||
- Note that it wmight 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.
|
||||
|
||||
### Building .app on Mac
|
||||
|
||||
1. Run the normal build steps
|
||||
2. XCode > Settings > Account and download your dev key
|
||||
- I don't have paid Apple Developer CA to test
|
||||
3. Set codesign_identity in RuneKit.spec or leave it None for ad-hoc sign
|
||||
5. `poetry run make dist/RuneKit.app.zip`
|
||||
|
||||
## License
|
||||
|
||||
This project is [No Maintenance Intended](https://unmaintained.tech/).
|
||||
This protect is [No Maintenance Intended](https://unmaintained.tech/).
|
||||
It is provided as-is and may not be actively maintained. There's no support, and no promise that pull requests will be
|
||||
reviewed and merged. In other word: I wrote this for my own use and there's no point in keeping it to myself so I'm sharing it.
|
||||
However, maintaining it as a proper open source project is an ongoing work that I don't have the bandwidth to do.
|
||||
However, making it a proper open source project is an ongoing work that I'm not willing to do.
|
||||
|
||||
This project is [licensed](LICENSE) under GPLv3, and contains code from [third parties](THIRD_PARTY_LICENSE.md).
|
||||
Contains code from the Alt1 application.
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['main.py'],
|
||||
pathex=['runekit'],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=['tkinter'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=True)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='RuneKitApp',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True,
|
||||
upx=False,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=True,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
name='RuneKit')
|
||||
app = BUNDLE(coll,
|
||||
name='RuneKit.app',
|
||||
icon=None,
|
||||
bundle_identifier='de.cupco.runekit',
|
||||
info_plist={
|
||||
'LSEnvironment': {
|
||||
'LANG': 'en_US.UTF-8',
|
||||
'LC_CTYPE': 'en_US.UTF-8'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
from Cython.Build import cythonize
|
||||
|
||||
# use cythonize to build the extensions
|
||||
modules = [
|
||||
"runekit/image/_utils.pyx",
|
||||
"runekit/image/_chat.pyx",
|
||||
"runekit/image/_font.pyx",
|
||||
]
|
||||
|
||||
extensions = cythonize(modules)
|
||||
|
||||
|
||||
def build(setup_kwargs):
|
||||
"""Needed for the poetry building interface."""
|
||||
|
||||
setup_kwargs.update(
|
||||
{
|
||||
"ext_modules": extensions,
|
||||
}
|
||||
)
|
||||
@@ -1,62 +0,0 @@
|
||||
# Alt1 API
|
||||
|
||||
- [ ] alt1.overlay
|
||||
- [ ] alt1.captureMethod
|
||||
- [ ] alt1.permission*
|
||||
- [x] alt1.lastWorldHop
|
||||
- [x] alt1.currentWorld
|
||||
- [ ] alt1.rsPing
|
||||
- [ ] alt1.rsFps
|
||||
- [ ] alt1.openInfo
|
||||
- [x] alt1.screen*
|
||||
- [x] alt1.captureInterval
|
||||
- [x] alt1.mousePosition
|
||||
- [x] alt1.rsLinked
|
||||
- [x] alt1.rsWidth, rsHeight, rsScaling
|
||||
- [x] alt1.rsActive
|
||||
- [x] alt1.userResize
|
||||
- [ ] alt1.identiyAppUrl
|
||||
- [x] alt1.openBrowser
|
||||
- [x] alt1.clearBinds
|
||||
- [ ] alt1.registerStatusDaemon
|
||||
- [ ] alt1.getStatusDaemonState
|
||||
- [x] alt1.showNotification
|
||||
- [x] alt1.closeApp
|
||||
- [x] alt1.setTooltip
|
||||
- [x] alt1.clearTooltip
|
||||
- [x] alt1.setTaskbarProgress
|
||||
- [ ] alt1.setTitleBarText
|
||||
- [x] alt1.overLayRect
|
||||
- [x] alt1.overLayText
|
||||
- [x] alt1.overLayTextEx
|
||||
- [x] alt1.overLayLine
|
||||
- [x] alt1.overLayImage
|
||||
- [x] alt1.overLayClearGroup
|
||||
- [x] alt1.overLaySetGroup
|
||||
- [x] alt1.overLayFreezeGroup
|
||||
- [x] alt1.overLayContinueGroup
|
||||
- [x] alt1.overLayRefreshGroup
|
||||
- [x] alt1.overLaySetGroupZIndex
|
||||
- [x] alt1.getRegion
|
||||
- [ ] alt1.getRegionMulti
|
||||
- [x] alt1.bindRegion
|
||||
- [ ] alt1.bindScreenRegion
|
||||
- [x] alt1.bindGetRegion
|
||||
- [ ] alt1.bindReadString
|
||||
- [ ] alt1.bindReadColorString
|
||||
- [ ] alt1.bindReadStringEx
|
||||
- [ ] alt1.bindReadRightClickString
|
||||
- [ ] alt1.bindGetPixel
|
||||
- [ ] alt1.bindFindSubImg
|
||||
|
||||
# Alt1 Events
|
||||
|
||||
- [x] alt1pressed
|
||||
- [ ] menudetected
|
||||
- [ ] permissionchanged
|
||||
- [ ] daemonrun
|
||||
- [ ] userevent
|
||||
- [x] rsfocus
|
||||
- [x] blur
|
||||
|
||||
rslinked, rsunlinked probably not going to be implemented
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=RuneKit
|
||||
Exec=runekit
|
||||
Comment=Alt1-compatible toolbox for RuneScape 3
|
||||
Icon=python
|
||||
Categories=Game;
|
||||
@@ -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 $@
|
||||
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,75 +0,0 @@
|
||||
# MacOS Setup (for programming novices)
|
||||
|
||||
## Packaged release
|
||||
|
||||
[Grab the packaged .app](https://github.com/whs/runekit/releases/tag/platypus-initial)
|
||||
|
||||
Note that it might be outdated from the source release
|
||||
|
||||
## Running from source
|
||||
|
||||
This guide is assuming that you're willing install a few things and run a few commands in your `terminal` application.
|
||||
|
||||
Not sure how to open the terminal? You can try this [link](https://lmgtfy.app/?q=how+to+open+terminal+on+mac).
|
||||
|
||||
Got your terminal open? Great, time to run some commands.
|
||||
|
||||
### (1/4) Downloading this code
|
||||
|
||||
But first, we need to download this code. If you know how to use `git` to do that, great. If that last sentence made no sense, keep reading.
|
||||
|
||||
We're going to begin by downloading the code as a zip file. You can do this by clicking the green button on the GitHub repo page then clicking `Download Zip`, as shown below:
|
||||
|
||||

|
||||
|
||||
Then, go ahead and open it with `Archive Utitlity`:
|
||||
|
||||

|
||||
|
||||
You should now find the folder in your `Downloads` folder (use `Finder` to find it):
|
||||
|
||||

|
||||
|
||||
Great, we have the code! Next step please.
|
||||
|
||||
### (2/4) Installing Homebrew (a package manager)
|
||||
|
||||
Basically, follow the instructions on their (Homebrew's) website [here](https://brew.sh/).
|
||||
|
||||
Copy the command into your terminal prompt, then hit `enter`:
|
||||
|
||||

|
||||
|
||||
You can check that this worked by closing and reopening your terminal, then running `which brew`. You should get the following output:
|
||||
|
||||

|
||||
|
||||
Awesome, we'll need this tool later. For now, on to Python.
|
||||
|
||||
### (3/4) Installing Python (3.9.1)
|
||||
|
||||
Alright, we're going to install Python the straightforward way. We're going to do that by visiting Python's website [here](https://www.python.org/downloads/release/python-391/) and downloading + running the `macOS 64-bit` installer (which of the two depends on what type of mac you have).
|
||||
|
||||

|
||||
|
||||
If you're having trouble finding the downloads at the link, remember to scroll to the bottom of the page.
|
||||
|
||||
### (4/4) Installing poetry
|
||||
|
||||
Run the following command in your terminal:
|
||||
|
||||
```bash
|
||||
brew install poetry
|
||||
```
|
||||
|
||||
### One last thing...
|
||||
|
||||
Before you go and run the commands from the other page, you'll want to change what file directory your terminal is in to be the folder where you downloaded `runekit-master`. Assuming you downloaded it as instructed in step 1, you can do this by running the following:
|
||||
|
||||
```bash
|
||||
cd ~/Downloads/runekit-master
|
||||
```
|
||||
|
||||
**Note**: if you close and reopen the terminal, you'll need to renavigate to the correct folder (just rerun the above command).
|
||||
|
||||
That simple. Now you can go back to the other file and pick up where you left off, [here](../README.md#running).
|
||||
@@ -1,4 +1,44 @@
|
||||
from runekit.main import main
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import click
|
||||
from PySide2.QtWebEngine import QtWebEngine
|
||||
from PySide2.QtWidgets import QApplication
|
||||
|
||||
from runekit.browser import init
|
||||
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")
|
||||
def main(app_url, game_index, qt_args):
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logging.info("Starting QtWebEngine")
|
||||
QtWebEngine.initialize()
|
||||
init()
|
||||
app = QApplication([sys.argv[0], *qt_args])
|
||||
host = Host()
|
||||
|
||||
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))
|
||||
game = game_instances[game_index]
|
||||
|
||||
logging.info("Loading app")
|
||||
host.start_app(app_url, game)
|
||||
|
||||
app.exec_()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +1,3 @@
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.2"
|
||||
description = "Python graph (network) package"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "appdirs"
|
||||
version = "1.4.4"
|
||||
@@ -38,71 +30,43 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.10.8"
|
||||
version = "2020.12.5"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.14.6"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.0.6"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.0.2"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
description = "Cross-platform colored terminal text."
|
||||
name = "chardet"
|
||||
version = "4.0.0"
|
||||
description = "Universal encoding detector for Python 2 and 3"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "7.1.2"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "cython"
|
||||
version = "0.29.21"
|
||||
description = "The Cython compiler for writing C extensions for the Python language."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.2"
|
||||
version = "2.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.15.2"
|
||||
description = "Mach-O header analysis and editing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = ">=0.15"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
@@ -112,88 +76,25 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "1.21.2"
|
||||
description = "NumPy is the fundamental package for array computing with Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<3.11"
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.5.3.56"
|
||||
description = "Wrapper package for OpenCV python bindings."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
numpy = ">=1.21.0"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.9.0"
|
||||
version = "0.8.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "8.3.2"
|
||||
version = "8.1.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "5.8.0"
|
||||
description = "Cross-platform lib for process and system monitoring in Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.extras]
|
||||
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.20"
|
||||
description = "C parser in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "4.5.1"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
pyinstaller-hooks-contrib = ">=2020.6"
|
||||
|
||||
[package.extras]
|
||||
encryption = ["tinyaes (>=1.0.0)"]
|
||||
hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2021.3"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-core"
|
||||
version = "7.3"
|
||||
version = "7.1"
|
||||
description = "Python<->ObjC Interoperability Module"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -201,52 +102,52 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-applicationservices"
|
||||
version = "7.3"
|
||||
version = "7.1"
|
||||
description = "Wrappers for the framework ApplicationServices on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.3"
|
||||
pyobjc-framework-Cocoa = ">=7.3"
|
||||
pyobjc-framework-Quartz = ">=7.3"
|
||||
pyobjc-core = ">=7.1"
|
||||
pyobjc-framework-Cocoa = ">=7.1"
|
||||
pyobjc-framework-Quartz = ">=7.1"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "7.3"
|
||||
version = "7.1"
|
||||
description = "Wrappers for the Cocoa frameworks on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.3"
|
||||
pyobjc-core = ">=7.1"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-coretext"
|
||||
version = "7.3"
|
||||
version = "7.1"
|
||||
description = "Wrappers for the framework CoreText on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.3"
|
||||
pyobjc-framework-Cocoa = ">=7.3"
|
||||
pyobjc-framework-Quartz = ">=7.3"
|
||||
pyobjc-core = ">=7.1"
|
||||
pyobjc-framework-Cocoa = ">=7.1"
|
||||
pyobjc-framework-Quartz = ">=7.1"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-quartz"
|
||||
version = "7.3"
|
||||
version = "7.1"
|
||||
description = "Wrappers for the Quartz frameworks on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.3"
|
||||
pyobjc-framework-Cocoa = ">=7.3"
|
||||
pyobjc-core = ">=7.1"
|
||||
pyobjc-framework-Cocoa = ">=7.1"
|
||||
|
||||
[[package]]
|
||||
name = "pyside2"
|
||||
@@ -259,9 +160,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10"
|
||||
[package.dependencies]
|
||||
shiboken2 = "5.15.2"
|
||||
|
||||
[[package]]
|
||||
name = "python-xlib"
|
||||
version = "0.29"
|
||||
description = "Python X Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2021.10.8"
|
||||
version = "2020.11.13"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -269,21 +181,21 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.26.0"
|
||||
version = "2.25.1"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||
chardet = ">=3.0.2,<5"
|
||||
idna = ">=2.5,<3"
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[[package]]
|
||||
name = "shiboken2"
|
||||
@@ -295,20 +207,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
version = "1.15.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "sysv-ipc"
|
||||
version = "1.1.0"
|
||||
description = "System V IPC primitives (semaphores, shared memory and message queues) for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
@@ -319,7 +223,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
version = "1.4.3"
|
||||
version = "1.4.2"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -327,7 +231,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.10.0.2"
|
||||
version = "3.7.4.3"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -335,7 +239,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.7"
|
||||
version = "1.26.2"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -346,28 +250,12 @@ brotli = ["brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "xcffib"
|
||||
version = "0.11.1"
|
||||
description = "A drop in replacement for xpyb, an XCB python binding"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.1.0"
|
||||
six = "*"
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9,<3.10"
|
||||
content-hash = "6d7ef5bf604ccfd42e5b3251cf4e6bc5e1ee5d89294bb39f72f984abeb1dd43f"
|
||||
content-hash = "7499ffeaf18341e92d2e7b1af5f9b0a21be4b702f988fad58b589708499aed06"
|
||||
|
||||
[metadata.files]
|
||||
altgraph = [
|
||||
{file = "altgraph-0.17.2-py2.py3-none-any.whl", hash = "sha256:743628f2ac6a7c26f5d9223c91ed8ecbba535f506f4b6f558885a8a56a105857"},
|
||||
{file = "altgraph-0.17.2.tar.gz", hash = "sha256:ebf2269361b47d97b3b88e696439f6e4cbc607c17c51feb1754f90fb79839158"},
|
||||
]
|
||||
appdirs = [
|
||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
||||
@@ -376,283 +264,135 @@ black = [
|
||||
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
|
||||
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"},
|
||||
{file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"},
|
||||
{file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"},
|
||||
{file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"},
|
||||
{file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"},
|
||||
{file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"},
|
||||
{file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"},
|
||||
{file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"},
|
||||
{file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"},
|
||||
{file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"},
|
||||
{file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"},
|
||||
{file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"},
|
||||
{file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"},
|
||||
{file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"},
|
||||
{file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"},
|
||||
{file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"},
|
||||
{file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
|
||||
{file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
|
||||
chardet = [
|
||||
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
||||
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.0.2-py3-none-any.whl", hash = "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8"},
|
||||
{file = "click-8.0.2.tar.gz", hash = "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5"},
|
||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
cython = [
|
||||
{file = "Cython-0.29.21-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c541b2b49c6638f2b5beb9316726db84a8d1c132bf31b942dae1f9c7f6ad3b92"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b8d8497091c1dc8705d1575c71e908a93b1f127a174b2d472020f3d84263ac28"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:695a6bcaf9e12b1e471dfce96bbecf22a1487adc2ac6106b15960a2b51b97f5d"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-win32.whl", hash = "sha256:171b9f70ceafcec5852089d0f9c1e75b0d554f46c882cd4e2e4acaba9bd7d148"},
|
||||
{file = "Cython-0.29.21-cp27-cp27m-win_amd64.whl", hash = "sha256:539e59949aab4955c143a468810123bf22d3e8556421e1ce2531ed4893914ca0"},
|
||||
{file = "Cython-0.29.21-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e93acd1f603a0c1786e0841f066ae7cef014cf4750e3cd06fd03cfdf46361419"},
|
||||
{file = "Cython-0.29.21-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:2922e3031ba9ebbe7cb9200b585cc33b71d66023d78450dcb883f824f4969371"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:497841897942f734b0abc2dead2d4009795ee992267a70a23485fd0e937edc0b"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0ac10bf476476a9f7ef61ec6e44c280ef434473124ad31d3132b720f7b0e8d2a"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-win32.whl", hash = "sha256:31c71a615f38401b0dc1f2a5a9a6c421ffd8908c4cd5bbedc4014c1b876488e8"},
|
||||
{file = "Cython-0.29.21-cp34-cp34m-win_amd64.whl", hash = "sha256:c4b78356074fcaac04ecb4de289f11d506e438859877670992ece11f9c90f37b"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:b2f9172e4d6358f33ecce6a4339b5960f9f83eab67ea244baa812737793826b7"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:856c7fb31d247ce713d60116375e1f8153d0291ab5e92cca7d8833a524ba9991"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:715294cd2246b39a8edca464a8366eb635f17213e4a6b9e74e52d8b877a8cb63"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-win32.whl", hash = "sha256:23f3a00b843a19de8bb4468b087db5b413a903213f67188729782488d67040e0"},
|
||||
{file = "Cython-0.29.21-cp35-cp35m-win_amd64.whl", hash = "sha256:ccb77faeaad99e99c6c444d04862c6cf604204fe0a07d4c8f9cbf2c9012d7d5a"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e272ed97d20b026f4f25a012b25d7d7672a60e4f72b9ca385239d693cd91b2d5"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:8c6e25e9cc4961bb2abb1777c6fa9d0fa2d9b014beb3276cebe69996ff162b78"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:57ead89128dee9609119c93d3926c7a2add451453063147900408a50144598c6"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-win32.whl", hash = "sha256:0e25c209c75df8785480dcef85db3d36c165dbc0f4c503168e8763eb735704f2"},
|
||||
{file = "Cython-0.29.21-cp36-cp36m-win_amd64.whl", hash = "sha256:a0674f246ad5e1571ef29d4c5ec1d6ecabe9e6c424ad0d6fee46b914d5d24d69"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5da187bebe38030325e1c0b5b8a804d489410be2d384c0ef3ba39493c67eb51e"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9ce5e5209f8406ffc2b058b1293cce7a954911bb7991e623564d489197c9ba30"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5e545a48f919e40079b0efe7b0e081c74b96f9ef25b9c1ff4cdbd95764426b58"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-win32.whl", hash = "sha256:c8435959321cf8aec867bbad54b83b7fb8343204b530d85d9ea7a1f5329d5ac2"},
|
||||
{file = "Cython-0.29.21-cp37-cp37m-win_amd64.whl", hash = "sha256:540b3bee0711aac2e99bda4fa0a46dbcd8c74941666bfc1ef9236b1a64eeffd9"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93f5fed1c9445fb7afe20450cdaf94b0e0356d47cc75008105be89c6a2e417b1"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9207fdedc7e789a3dcaca628176b80c82fbed9ae0997210738cbb12536a56699"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:603b9f1b8e93e8b494d3e89320c410679e21018e48b6cbc77280f5db71f17dc0"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-win32.whl", hash = "sha256:473df5d5e400444a36ed81c6596f56a5b52a3481312d0a48d68b777790f730ae"},
|
||||
{file = "Cython-0.29.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8a8a31b9e8860634adbca30fea1d0c7f08e208b3d7611f3e580e5f20992e5d7"},
|
||||
{file = "Cython-0.29.21-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7ebaa8800c376bcdae596fb1372cb4232a5ef957619d35839520d2786f2debb9"},
|
||||
{file = "Cython-0.29.21-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c111ac9abdf715762e4fb87395e59d61c0fbb6ce79eb2e24167700b6cfa8ba79"},
|
||||
{file = "Cython-0.29.21-py2.py3-none-any.whl", hash = "sha256:5c4276fdcbccdf1e3c1756c7aeb8395e9a36874fa4d30860e7694f43d325ae13"},
|
||||
{file = "Cython-0.29.21.tar.gz", hash = "sha256:e57acb89bd55943c8d8bf813763d20b9099cc7165c0f16b707631a7654be9cad"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
|
||||
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
|
||||
]
|
||||
macholib = [
|
||||
{file = "macholib-1.15.2-py2.py3-none-any.whl", hash = "sha256:885613dd02d3e26dbd2b541eb4cc4ce611b841f827c0958ab98656e478b9e6f6"},
|
||||
{file = "macholib-1.15.2.tar.gz", hash = "sha256:1542c41da3600509f91c165cb897e7e54c0e74008bd8da5da7ebbee519d593d2"},
|
||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
numpy = [
|
||||
{file = "numpy-1.21.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52a664323273c08f3b473548bf87c8145b7513afd63e4ebba8496ecd3853df13"},
|
||||
{file = "numpy-1.21.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a7b9db0a2941434cd930dacaafe0fc9da8f3d6157f9d12f761bbde93f46218"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f2dc79c093f6c5113718d3d90c283f11463d77daa4e83aeeac088ec6a0bda52"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a55e4d81c4260386f71d22294795c87609164e22b28ba0d435850fbdf82fc0c5"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:426a00b68b0d21f2deb2ace3c6d677e611ad5a612d2c76494e24a562a930c254"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:298156f4d3d46815eaf0fcf0a03f9625fc7631692bd1ad851517ab93c3168fc6"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09858463db6dd9f78b2a1a05c93f3b33d4f65975771e90d2cf7aadb7c2f66edf"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:805459ad8baaf815883d0d6f86e45b3b0b67d823a8f3fa39b1ed9c45eaf5edf1"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-win32.whl", hash = "sha256:f545c082eeb09ae678dd451a1b1dbf17babd8a0d7adea02897a76e639afca310"},
|
||||
{file = "numpy-1.21.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b160b9a99ecc6559d9e6d461b95c8eec21461b332f80267ad2c10394b9503496"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a5109345f5ce7ddb3840f5970de71c34a0ff7fceb133c9441283bb8250f532a3"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:209666ce9d4a817e8a4597cd475b71b4878a85fa4b8db41d79fdb4fdee01dde2"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c01b59b33c7c3ba90744f2c695be571a3bd40ab2ba7f3d169ffa6db3cfba614f"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e42029e184008a5fd3d819323345e25e2337b0ac7f5c135b7623308530209d57"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7fdc7689daf3b845934d67cb221ba8d250fdca20ac0334fea32f7091b93f00d3"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550564024dc5ceee9421a86fc0fb378aa9d222d4d0f858f6669eff7410c89bef"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf75d5825ef47aa51d669b03ce635ecb84d69311e05eccea083f31c7570c9931"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a9da45b748caad72ea4a4ed57e9cd382089f33c5ec330a804eb420a496fa760f"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-win32.whl", hash = "sha256:e167b9805de54367dcb2043519382be541117503ce99e3291cc9b41ca0a83557"},
|
||||
{file = "numpy-1.21.2-cp38-cp38-win_amd64.whl", hash = "sha256:466e682264b14982012887e90346d33435c984b7fead7b85e634903795c8fdb0"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dd0e3651d210068d13e18503d75aaa45656eef51ef0b261f891788589db2cc38"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92a0ab128b07799dd5b9077a9af075a63467d03ebac6f8a93e6440abfea4120d"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fde50062d67d805bc96f1a9ecc0d37bfc2a8f02b937d2c50824d186aa91f2419"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:640c1ccfd56724f2955c237b6ccce2e5b8607c3bc1cc51d3933b8c48d1da3723"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5de64950137f3a50b76ce93556db392e8f1f954c2d8207f78a92d1f79aa9f737"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b342064e647d099ca765f19672696ad50c953cac95b566af1492fd142283580f"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-win32.whl", hash = "sha256:30fc68307c0155d2a75ad19844224be0f2c6f06572d958db4e2053f816b859ad"},
|
||||
{file = "numpy-1.21.2-cp39-cp39-win_amd64.whl", hash = "sha256:b5e8590b9245803c849e09bae070a8e1ff444f45e3f0bed558dd722119eea724"},
|
||||
{file = "numpy-1.21.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d96a6a7d74af56feb11e9a443150216578ea07b7450f7c05df40eec90af7f4a7"},
|
||||
{file = "numpy-1.21.2.zip", hash = "sha256:423216d8afc5923b15df86037c6053bf030d15cc9e3224206ef868c2d63dd6dc"},
|
||||
]
|
||||
opencv-python-headless = [
|
||||
{file = "opencv-python-headless-4.5.3.56.tar.gz", hash = "sha256:3308d22efec8a69932709fc76456ef7e0d1cd9eb2272d94b7e5df0674d3e324f"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b563106e93525bd30072a27448cc2b5c11e45bf6025e74e1d40efd5349a31736"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:ecd84834f58daf94d3a4874a11997180380ef9af11b1fd5f81dbb8599b3c3446"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0ef768d119a00eb6be0ba7953ab875218084d078db564af89af2b3891b6d7a3b"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-win32.whl", hash = "sha256:d70f3a44fb25f555da36e6c242cd1dffac6c4637eb900584f2a70a46e8e3cf05"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp36-cp36m-win_amd64.whl", hash = "sha256:3599939dc61ab32c91cb2dbfd80a8199b53bf8a08cbb636ee9c2f27d74fe4f8f"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:3b1aa94894872e17a5fa07794562d8c44add78dd4c90a12fa1d946789e088784"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:fc9fa0fa88423c0e74db52fe13939aa506789f924a2a879e31f1d415e39a0db2"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90686bd1292972ad47298154c8ad37543080b2fc0417b68a350d30232b77f670"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:c54ad51bfc4623de287bd68f14f481f72300bd16a2d9e14850c1ef1a2dfc7dca"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-win32.whl", hash = "sha256:fd0f0629e27d97eec2f51500afbe0845876fdcd89d412035f4d7beb618c51066"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp37-cp37m-win_amd64.whl", hash = "sha256:b224afffbc4ea21fe100824ff08a79ad1fae3088c1280b8ff815067b827e5240"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:bfe8989ed4af5e14ef1480c89fe259f5d4bc66c5ae0e7d56d7c778ef6ebbcdf4"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1a00d01d8d7250d6b87c63eb7b2df1e541fdb10919c22940cc53884bb12057c9"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:39eaae19d5fa906c7ec2db76447c0ff9df6305633493f1b86e4c0080ed1cb036"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:e19ba664cae0bc6ad39e2fbf3b3f16c93009132f792dcb7ca0105f0cc15997ed"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-win32.whl", hash = "sha256:ebfeaff7e0bb1c293008a49b2b2c033a7cec3805006a8284fa1a44ad8c30446b"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp38-cp38-win_amd64.whl", hash = "sha256:0efe30ce934ec2ab7272143d17f68885910a715c6aeffd02973c04b3d8b99ca6"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:0e8080dc5baf6855b663634d9c7a45732e2999bbe44aa5e4a14c15f3631998e4"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:465b157480902ecd6b2c4c46d547e4cfdec7595177bcc612c6f4c351966a5a1e"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d4c784339c366d27d0b4503a073c97a7cd78657937b3755fdbc9168a002f6409"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c7a1d365f2f8ce413847479a2d39bee2be6d5027119b08c4c5f6594f1f57673d"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-win32.whl", hash = "sha256:0935bf569e2edc1af8bc38f00813e37b1ba63e4b2eb0dc48651f405068b8b36d"},
|
||||
{file = "opencv_python_headless-4.5.3.56-cp39-cp39-win_amd64.whl", hash = "sha256:f1082f4f15a681b435686ed3fc0232128a222fed6792a47fae5cf081eb751ccf"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
|
||||
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"},
|
||||
{file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"},
|
||||
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"},
|
||||
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"},
|
||||
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"},
|
||||
{file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"},
|
||||
{file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"},
|
||||
{file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"},
|
||||
{file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"},
|
||||
{file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"},
|
||||
{file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"},
|
||||
{file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"},
|
||||
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"},
|
||||
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"},
|
||||
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"},
|
||||
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"},
|
||||
{file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"},
|
||||
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"},
|
||||
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"},
|
||||
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"},
|
||||
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"},
|
||||
{file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"},
|
||||
{file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"},
|
||||
]
|
||||
psutil = [
|
||||
{file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
|
||||
{file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"},
|
||||
{file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"},
|
||||
{file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"},
|
||||
{file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"},
|
||||
{file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"},
|
||||
{file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"},
|
||||
{file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"},
|
||||
{file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"},
|
||||
{file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"},
|
||||
{file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"},
|
||||
{file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"},
|
||||
{file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"},
|
||||
{file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"},
|
||||
{file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"},
|
||||
{file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"},
|
||||
{file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"},
|
||||
{file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"},
|
||||
{file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"},
|
||||
{file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"},
|
||||
{file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"},
|
||||
{file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"},
|
||||
{file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"},
|
||||
{file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"},
|
||||
{file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"},
|
||||
{file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"},
|
||||
{file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
|
||||
{file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
|
||||
]
|
||||
pycparser = [
|
||||
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
|
||||
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
|
||||
]
|
||||
pyinstaller = [
|
||||
{file = "pyinstaller-4.5.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ecc2baadeeefd2b6fbf39d13c65d4aa603afdda1c6aaaebc4577ba72893fee9e"},
|
||||
{file = "pyinstaller-4.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4d848cd782ee0893d7ad9fe2bfe535206a79f0b6760cecc5f2add831258b9322"},
|
||||
{file = "pyinstaller-4.5.1-py3-none-manylinux2014_i686.whl", hash = "sha256:8f747b190e6ad30e2d2fd5da9a64636f61aac8c038c0b7f685efa92c782ea14f"},
|
||||
{file = "pyinstaller-4.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c587da8f521a7ce1b9efb4e3d0117cd63c92dc6cedff24590aeef89372f53012"},
|
||||
{file = "pyinstaller-4.5.1-py3-none-win32.whl", hash = "sha256:fed9f5e4802769a416a8f2ca171c6be961d1861cc05a0b71d20dfe05423137e9"},
|
||||
{file = "pyinstaller-4.5.1-py3-none-win_amd64.whl", hash = "sha256:aae456205c68355f9597411090576bb31b614e53976b4c102d072bbe5db8392a"},
|
||||
{file = "pyinstaller-4.5.1.tar.gz", hash = "sha256:30733baaf8971902286a0ddf77e5499ac5f7bf8e7c39163e83d4f8c696ef265e"},
|
||||
]
|
||||
pyinstaller-hooks-contrib = [
|
||||
{file = "pyinstaller-hooks-contrib-2021.3.tar.gz", hash = "sha256:169b09802a19f83593114821d6ba0416a05c7071ef0ca394f7bfb7e2c0c916c8"},
|
||||
{file = "pyinstaller_hooks_contrib-2021.3-py2.py3-none-any.whl", hash = "sha256:a52bc3834281266bbf77239cfc9521923336ca622f44f90924546ed6c6d3ad5e"},
|
||||
{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"},
|
||||
]
|
||||
pyobjc-core = [
|
||||
{file = "pyobjc-core-7.3.tar.gz", hash = "sha256:5081aedf8bb40aac1a8ad95adac9e44e148a882686ded614adf46bb67fd67574"},
|
||||
{file = "pyobjc_core-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e93ad769a20b908778fe950f62a843a6d8f0fa71996e5f3cc9fab5ae7d17771"},
|
||||
{file = "pyobjc_core-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f63fd37bbf3785af4ddb2f86cad5ca81c62cfc7d1c0099637ca18343c3656c1"},
|
||||
{file = "pyobjc_core-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9b1311f72f2e170742a7ee3a8149f52c35158dc024a21e88d6f1e52ba5d718b"},
|
||||
{file = "pyobjc_core-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d5e12a0729dfd1d998a861998b422d0a3e41923d75ea229bacf31372c831d7b"},
|
||||
{file = "pyobjc_core-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:efdee8c4884405e0c0186c57f87d7bfaa0abc1f50b18e865db3caea3a1f329b9"},
|
||||
{file = "pyobjc-core-7.1.tar.gz", hash = "sha256:a0616d5d816b4471f8f782c3a9a8923d2cc85014d88ad4f7fec694be9e6ea349"},
|
||||
{file = "pyobjc_core-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9fb45c9916f2a03ecd6b9ecde4c35d1d0f1a590ae2ea2372f9d9a360226ac1d"},
|
||||
{file = "pyobjc_core-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff8e87358c6195a2937004f279050cce3d4c02cd77acd73c5ad367307def855"},
|
||||
{file = "pyobjc_core-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:afb38efd3f2960eb49eb78552d465cfd025a9d6efa06cd4cd8694dafbe7c6e06"},
|
||||
{file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cb329c4119044fe83bcb3c5d4794d636c706ff0cb7c1c77d36ef5c373100082"},
|
||||
{file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7913d7b20217c294900537faf58e5cc15942ed7af277bf05db25667d18255114"},
|
||||
]
|
||||
pyobjc-framework-applicationservices = [
|
||||
{file = "pyobjc-framework-ApplicationServices-7.3.tar.gz", hash = "sha256:1925ac30a817e557d1c08450005103bbf76ebd3ff473631fe9875070377b0b4d"},
|
||||
{file = "pyobjc_framework_ApplicationServices-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:daa4a9c51a927630fdd3d3f627e03ebc370aee3c397305db85a0a8ba4c28ae93"},
|
||||
{file = "pyobjc_framework_ApplicationServices-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:167aa21ee47b0ee6e4e399915371d183ae84880dc3813c27519e759acb9d20c9"},
|
||||
{file = "pyobjc_framework_ApplicationServices-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a98f0f1e21465868f9dd32588ae71e5e6a4cb5c434d4158c9e12273fd7b8f27"},
|
||||
{file = "pyobjc_framework_ApplicationServices-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2d55796610e6293e83cc40183347e7f75a7c0682775cc19e5986945efa9cac1b"},
|
||||
{file = "pyobjc_framework_ApplicationServices-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:afd1ef147447fe7b06a271458eabb37ece6436705abf86265d7fb57310eca45f"},
|
||||
{file = "pyobjc-framework-ApplicationServices-7.1.tar.gz", hash = "sha256:84d790bafa256d05121950554645f9edd2ba4c59fc2684dfdfa701e8ad28baf6"},
|
||||
{file = "pyobjc_framework_ApplicationServices-7.1-py2.py3-none-any.whl", hash = "sha256:ef8c7729e0c5b676f3bcafff3fde3714f938b821263cc3528db0cd37c3845991"},
|
||||
]
|
||||
pyobjc-framework-cocoa = [
|
||||
{file = "pyobjc-framework-Cocoa-7.3.tar.gz", hash = "sha256:b18d05e7a795a3455ad191c3e43d6bfa673c2a4fd480bb1ccf57191051b80b7e"},
|
||||
{file = "pyobjc_framework_Cocoa-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9edffdfa6dd1f71f21b531c3e61fdd3e4d5d3bf6c5a528c98e88828cd60bac11"},
|
||||
{file = "pyobjc_framework_Cocoa-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:35a6340437a4e0109a302150b7d1f6baf57004ccf74834f9e6062fcafe2fd8d7"},
|
||||
{file = "pyobjc_framework_Cocoa-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c3886f2608ab3ed02482f8b2ebf9f782b324c559e84b52cfd92dba8a1109872"},
|
||||
{file = "pyobjc_framework_Cocoa-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e8e7a1a82cca21d9bfac9115baf065305f3da577bf240085964dfb9c9fff337"},
|
||||
{file = "pyobjc_framework_Cocoa-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c15f43077c9a2ba1853eb402ff7a9515df9e584315bc2fcb779d4c95ef46dc5"},
|
||||
{file = "pyobjc-framework-Cocoa-7.1.tar.gz", hash = "sha256:67966152b3d38a0225176fceca2e9f56d849c8e7445548da09a00cb13155ec3e"},
|
||||
{file = "pyobjc_framework_Cocoa-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bef77eafaac5eaf1d91d479d5483fd02216caa3edc27e8f5adc9af0b3fecdac3"},
|
||||
{file = "pyobjc_framework_Cocoa-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2ea3582c456827dc20e648c905fdbcf8d3dfae89434f981e9b761cd07262049"},
|
||||
{file = "pyobjc_framework_Cocoa-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a4050f2d776f40c2409a151c6f7896420e936934b3bdbfabedf91509637ed9b"},
|
||||
{file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f68f022f1f6d5985c418e10c6608c562fcf4bfe3714ec64fd10ce3dc6221bd4"},
|
||||
{file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ecfefd4c48dae42275c18679c69f6f2fff970e711097515a0a8732fc10194018"},
|
||||
]
|
||||
pyobjc-framework-coretext = [
|
||||
{file = "pyobjc-framework-CoreText-7.3.tar.gz", hash = "sha256:5b5fc91bcbd2fe5199f6b65971d62bea02f942c76d6acb59168c041c7af435d9"},
|
||||
{file = "pyobjc_framework_CoreText-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a3ae27d5756b9d62d113e7c4f12022f8812bc95bc277f920f0fe2ca45b5272be"},
|
||||
{file = "pyobjc_framework_CoreText-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fd3f11b3358cbf5a56d4a01e736322daa5b6ce6e3701d41cc9eafcede0267faa"},
|
||||
{file = "pyobjc_framework_CoreText-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d290880894256497425a1abd116076e7a74c8b92a2b1b27d899ede16f52e5f20"},
|
||||
{file = "pyobjc_framework_CoreText-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b172b22f51625a0f92c9730b9d9a193f12bd5e56552d77b6841dfaac0d16bab0"},
|
||||
{file = "pyobjc_framework_CoreText-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:862e5c2c1bd6f5d276321c9a2d557dd8fb62ab1da55722a582b9e6621b05c1bd"},
|
||||
{file = "pyobjc-framework-CoreText-7.1.tar.gz", hash = "sha256:f3682299f02e75b4d5c7b0ede216e1cea3556ede0fc98a5319e7c2d359b140a7"},
|
||||
{file = "pyobjc_framework_CoreText-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:83b8bd84fa7666983ba2ed1d8cf7d0cc67e1c2190f5b59c1f026806da896146e"},
|
||||
{file = "pyobjc_framework_CoreText-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:171d3d60738b51b971ad4ea91016e9a2cc8164311b0aa2de34cf84da1faa7d30"},
|
||||
{file = "pyobjc_framework_CoreText-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d33565a158a511a2cd3ac4c15ececb5b6a033f8ead6d18ee688a6de147c52d1b"},
|
||||
{file = "pyobjc_framework_CoreText-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9c8eed9c876e723a93bdff78ffd767f9da6132562702b95f3b289a9618c196a3"},
|
||||
{file = "pyobjc_framework_CoreText-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd42eeaa4351c24fb3d12bf954ffb5c588d3a33ae363388c092e5bd4c5e10d27"},
|
||||
]
|
||||
pyobjc-framework-quartz = [
|
||||
{file = "pyobjc-framework-Quartz-7.3.tar.gz", hash = "sha256:98812844c34262def980bdf60923a875cd43428a8375b6fd53bd2cd800eccf0b"},
|
||||
{file = "pyobjc_framework_Quartz-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ef18f5a16511ded65980bf4f5983ea5d35c88224dbad1b3112abd29c60413ea"},
|
||||
{file = "pyobjc_framework_Quartz-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b41eec8d4b10c7c7e011e2f9051367f5499ef315ba52dfbae573c3a2e05469c"},
|
||||
{file = "pyobjc_framework_Quartz-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c65456ed045dfe1711d0298734e5a3ad670f8c770f7eb3b19979256c388bdd2"},
|
||||
{file = "pyobjc_framework_Quartz-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ddbca6b466584c3dc0e5b701b1c2a9b5d97ddc1d79a949927499ebb1be1f210"},
|
||||
{file = "pyobjc_framework_Quartz-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7aba3cd966a0768dd58a35680742820f0c5ac596a9cd11014e2057818e65b0af"},
|
||||
{file = "pyobjc-framework-Quartz-7.1.tar.gz", hash = "sha256:73102c9f4dbfa13275621014785ab3b684cf03ce93a4b0b270500c795349bea9"},
|
||||
{file = "pyobjc_framework_Quartz-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7207a26244f02d4534ebb007fa55a9dc7c1b7fbb490d1e89e0d62cfd175e20f3"},
|
||||
{file = "pyobjc_framework_Quartz-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5bc7a4fb3ea80b5af6910cc27729a0774a96327a69583fcf28057cb2ffce33ac"},
|
||||
{file = "pyobjc_framework_Quartz-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0469d60d4a79fc252f74adaa8177d2c680621d858c1b8ef19c411e903e2c892"},
|
||||
{file = "pyobjc_framework_Quartz-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:04953c031fc35020682bd4613b9b5a9688bdb9eab7ed76fd8dcf028783568b4f"},
|
||||
{file = "pyobjc_framework_Quartz-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d8e0c086faf649f86386d0ed99194c6d0704b602576e2b258532b635b510b790"},
|
||||
]
|
||||
pyside2 = [
|
||||
{file = "PySide2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:4f17a0161995678110447711d685fcd7b15b762810e8f00f6dc239bffb70a32e"},
|
||||
@@ -662,52 +402,56 @@ 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"},
|
||||
]
|
||||
python-xlib = [
|
||||
{file = "python-xlib-0.29.tar.gz", hash = "sha256:e4bcb756f4aa05be7b82ee21de0ba04d73414018727b42ebd9fbcf409ea75d13"},
|
||||
{file = "python_xlib-0.29-py2.py3-none-any.whl", hash = "sha256:044095d1b1a5eab5a79f8d0b66f811a9ac6acd038dd3bae00cb3dbe90b32a7e3"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2021.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072"},
|
||||
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361"},
|
||||
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e"},
|
||||
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01"},
|
||||
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc"},
|
||||
{file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f"},
|
||||
{file = "regex-2021.10.8-cp310-cp310-win32.whl", hash = "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7"},
|
||||
{file = "regex-2021.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-win32.whl", hash = "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f"},
|
||||
{file = "regex-2021.10.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-win32.whl", hash = "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432"},
|
||||
{file = "regex-2021.10.8-cp37-cp37m-win_amd64.whl", hash = "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-win32.whl", hash = "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593"},
|
||||
{file = "regex-2021.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-win32.whl", hash = "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b"},
|
||||
{file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"},
|
||||
{file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"},
|
||||
{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"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
|
||||
{file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
|
||||
{file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
|
||||
{file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
|
||||
{file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
|
||||
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
|
||||
]
|
||||
shiboken2 = [
|
||||
{file = "shiboken2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:03f41b0693b91c7f89627f1085a4ecbe8591c03f904118a034854d935e0e766c"},
|
||||
@@ -718,61 +462,51 @@ shiboken2 = [
|
||||
{file = "shiboken2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:14a33169cf1bd919e4c4c4408fffbcd424c919a3f702df412b8d72b694e4c1d5"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
sysv-ipc = [
|
||||
{file = "sysv_ipc-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ff6e3c98d8aa3e1ae5378d86869c4699c76f94318705d08085c97fb010770a31"},
|
||||
{file = "sysv_ipc-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:30611dccecd8fa0dc9f83d4d465e5047692df0ec9a095abef8b90ccc64afbdaf"},
|
||||
{file = "sysv_ipc-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4bbbe4e5ad5ab859e6d21fc8ff3e3b01082b959fa5c2cd76289dbcd56350c5f9"},
|
||||
{file = "sysv_ipc-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:61190b934b0277f7a189092dcabceee8d9736fd73de90fd9b50ea69f0bac4723"},
|
||||
{file = "sysv_ipc-1.1.0.tar.gz", hash = "sha256:0f063cbd36ec232032e425769ebc871f195a7d183b9af32f9901589ea7129ac3"},
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
|
||||
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
|
||||
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
|
||||
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
|
||||
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
|
||||
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
|
||||
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
|
||||
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
||||
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
|
||||
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
|
||||
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
|
||||
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
|
||||
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
|
||||
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
|
||||
]
|
||||
xcffib = [
|
||||
{file = "xcffib-0.11.1.tar.gz", hash = "sha256:12949cfe2e68c806efd57596bb9bf3c151f399d4b53e15d1101b2e9baaa66f5a"},
|
||||
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"},
|
||||
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
|
||||
]
|
||||
@@ -4,25 +4,25 @@ 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"
|
||||
requests = "^2.26.0"
|
||||
requests = "^2.25.1"
|
||||
PySide2 = "^5.15.2"
|
||||
Pillow = "^8.3.1"
|
||||
sysv-ipc = [{ version="^1.1.0", platform="linux" }]
|
||||
xcffib = [{ version="^0.11.1", platform="linux" }]
|
||||
pyobjc-framework-Quartz = [{ version="7.3", platform="darwin" }]
|
||||
pyobjc-framework-ApplicationServices = [{ version="7.3", platform="darwin" }]
|
||||
pyobjc-framework-CoreText = [{ version="7.3", platform="darwin" }]
|
||||
click = "^8.0.1"
|
||||
psutil = "^5.8.0"
|
||||
opencv-python-headless = "^4.5.3.56"
|
||||
Pillow = "^8.1.0"
|
||||
python-xlib = [{ version="^0.29", platform="linux" }]
|
||||
pyobjc-framework-Quartz = [{ version="7.1", platform="darwin" }]
|
||||
pyobjc-framework-ApplicationServices = [{ version="7.1", platform="darwin" }]
|
||||
pyobjc-framework-CoreText = [{ version="7.1", platform="darwin" }]
|
||||
click = "^7.1.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
Cython = "^0.29.21"
|
||||
black = "^20.8b1"
|
||||
pyinstaller = [{ version="^4.5.1", 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"
|
||||
@@ -1,22 +0,0 @@
|
||||
<!DOCTYPE RCC>
|
||||
<RCC version="1.0">
|
||||
<qresource>
|
||||
<file>runekit/browser/alt1.js</file>
|
||||
<file>runekit/ui/trayicon.png</file>
|
||||
<file>runekit/ui/skins/default/borderDrag.png</file>
|
||||
<file>runekit/ui/skins/default/exit.png</file>
|
||||
<file>runekit/ui/skins/default/exitHover.png</file>
|
||||
<file>runekit/ui/skins/default/settings.png</file>
|
||||
<file>runekit/ui/skins/default/settingsHover.png</file>
|
||||
<file>runekit/ui/skins/default/minimize.png</file>
|
||||
<file>runekit/ui/skins/default/minimizeHover.png</file>
|
||||
<file>runekit/ui/skins/default/borderTL.png</file>
|
||||
<file>runekit/ui/skins/default/borderT.png</file>
|
||||
<file>runekit/ui/skins/default/borderTR.png</file>
|
||||
<file>runekit/ui/skins/default/borderL.png</file>
|
||||
<file>runekit/ui/skins/default/borderR.png</file>
|
||||
<file>runekit/ui/skins/default/borderBL.png</file>
|
||||
<file>runekit/ui/skins/default/borderB.png</file>
|
||||
<file>runekit/ui/skins/default/borderBR.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -27,11 +27,17 @@ class RequestHandler(typing.TypedDict):
|
||||
handlerScript: str
|
||||
|
||||
|
||||
class RectLike(typing.TypedDict):
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
UnblendModes = typing.Literal["raw", "removebg", "blackbg"]
|
||||
|
||||
|
||||
CaptureMulti = typing.Dict[str, typing.Optional[RectLike]]
|
||||
class FontMeta(typing.TypedDict):
|
||||
basey: int
|
||||
spacewidth: int
|
||||
treshold: float
|
||||
color: typing.Tuple[int, int, int]
|
||||
unblendmode: UnblendModes
|
||||
shadow: bool
|
||||
chars: str
|
||||
seconds: str
|
||||
img: typing.Optional[str]
|
||||
bonus: typing.Optional[typing.Dict[str, int]]
|
||||
@@ -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,2 +1 @@
|
||||
from .app import App
|
||||
from .store import AppStore
|
||||
@@ -13,38 +13,20 @@ 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
|
||||
# self.game_instance.embed_window(self.window.windowHandle()) # FIXME
|
||||
return self.window
|
||||
|
||||
def get_api(self) -> Alt1Api:
|
||||
@@ -67,7 +49,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 +56,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 +63,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"])
|
||||
|
||||
|
||||
@@ -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}")
|
||||
@@ -1,101 +1,37 @@
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import Qt, Slot, QRect, QObject, QUrl
|
||||
from PySide2.QtGui import QIcon, QDesktopServices
|
||||
from PySide2.QtCore import Qt, Slot, QRect
|
||||
from PySide2.QtGui import QIcon
|
||||
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:
|
||||
from runekit.app.app import App
|
||||
|
||||
|
||||
class PageClass(QWebEnginePage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
|
||||
def javaScriptConsoleMessage(
|
||||
self,
|
||||
level: QWebEnginePage.JavaScriptConsoleMessageLevel,
|
||||
message: str,
|
||||
line_no: int,
|
||||
src_id: str,
|
||||
):
|
||||
logger = self.logger.getChild(self.url().toString())
|
||||
log = logger.debug
|
||||
if level == QWebEnginePage.InfoMessageLevel:
|
||||
log = logger.debug
|
||||
elif level == QWebEnginePage.WarningMessageLevel:
|
||||
log = logger.warning
|
||||
elif level == QWebEnginePage.ErrorMessageLevel:
|
||||
log = logger.error
|
||||
log("%s", message)
|
||||
|
||||
def acceptNavigationRequest(
|
||||
self, url: QUrl, type_: QWebEnginePage.NavigationType, is_main_frame: bool
|
||||
) -> bool:
|
||||
if not is_main_frame:
|
||||
return super().acceptNavigationRequest(url, type_, is_main_frame)
|
||||
|
||||
# This breaks AFK Warden popups
|
||||
# if (
|
||||
# type_ != QWebEnginePage.NavigationTypeTyped
|
||||
# and url.authority() != self.url().authority()
|
||||
# ):
|
||||
# # The web is only allowed pages on the same origin
|
||||
# # Cross origin pages would open in browser
|
||||
# if url.scheme() in ("http", "https"):
|
||||
# QDesktopServices.openUrl(url)
|
||||
|
||||
# return False
|
||||
|
||||
return super().acceptNavigationRequest(url, type_, is_main_frame)
|
||||
|
||||
|
||||
class BrowserWindow(GameSnapMixin, QMainWindow):
|
||||
class BrowserWindow(QMainWindow):
|
||||
app: "App"
|
||||
browser: QWebEngineView
|
||||
page_class = PageClass
|
||||
framed = False
|
||||
|
||||
def __init__(self, app: "App", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.app = app
|
||||
|
||||
if self.framed:
|
||||
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.frame = WindowFrame(parent=self)
|
||||
self.frame.on_exit.connect(self.close)
|
||||
self.setCentralWidget(self.frame)
|
||||
|
||||
self._setup_browser()
|
||||
self.setWindowTitle(self.app.manifest["appName"])
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
|
||||
def _setup_browser(self):
|
||||
self.browser = BrowserView(self)
|
||||
self.setCentralWidget(self.browser)
|
||||
|
||||
if hasattr(self, "frame"):
|
||||
self.frame.set_content(self.browser)
|
||||
else:
|
||||
self.setCentralWidget(self.browser)
|
||||
|
||||
self.browser.setPage(self.get_page())
|
||||
|
||||
def get_page(self) -> QWebEnginePage:
|
||||
page = self.page_class(self.app.get_web_profile(), self.browser)
|
||||
page = QWebEnginePage(self.app.get_web_profile(), self.browser)
|
||||
page.setWebChannel(Alt1WebChannel(app=self.app, parent=self.browser))
|
||||
page.geometryChangeRequested.connect(self.on_geometry_change)
|
||||
page.iconChanged.connect(self.on_icon_changed)
|
||||
page.windowCloseRequested.connect(self.on_window_close)
|
||||
page.featurePermissionRequested.connect(self.on_permission_request)
|
||||
return page
|
||||
self.browser.setPage(page)
|
||||
|
||||
@Slot(QRect)
|
||||
def on_geometry_change(self, size: QRect):
|
||||
@@ -109,50 +45,13 @@ class BrowserWindow(GameSnapMixin, QMainWindow):
|
||||
def on_window_close(self):
|
||||
self.close()
|
||||
|
||||
@Slot(QUrl, QWebEnginePage.Feature)
|
||||
def on_permission_request(self, origin: QUrl, feature: QWebEnginePage.Feature):
|
||||
if feature == QWebEnginePage.Notifications and self.app.has_permission(
|
||||
"overlay"
|
||||
):
|
||||
# FIXME: This doesn't really work - PySide2 doesn't have QWebEngineNotification
|
||||
self.browser.page().setFeaturePermission(
|
||||
origin, feature, QWebEnginePage.PermissionGrantedByUser
|
||||
)
|
||||
elif (
|
||||
feature in (QWebEnginePage.DesktopVideoCapture, QWebEnginePage.DesktopAudioVideoCapture)
|
||||
and self.app.has_permission('pixel')
|
||||
):
|
||||
self.browser.page().setFeaturePermission(
|
||||
origin, feature, QWebEnginePage.PermissionGrantedByUser
|
||||
)
|
||||
else:
|
||||
self.browser.page().setFeaturePermission(
|
||||
origin, feature, QWebEnginePage.PermissionDeniedByUser
|
||||
)
|
||||
|
||||
|
||||
class BrowserView(QWebEngineView):
|
||||
def createWindow(self, type_: QWebEnginePage.WebWindowType) -> QWebEngineView:
|
||||
from runekit.app.view.popup_window import PopupWindow
|
||||
|
||||
app_window = self.browser_window()
|
||||
app_window = typing.cast(BrowserWindow, self.parent())
|
||||
popup = PopupWindow(app=app_window.app, parent=app_window)
|
||||
popup.setWindowIcon(app_window.windowIcon())
|
||||
|
||||
def show(ok: bool):
|
||||
popup.browser.loadFinished.disconnect(show)
|
||||
if ok:
|
||||
popup.show()
|
||||
else:
|
||||
popup.close()
|
||||
|
||||
popup.browser.loadFinished.connect(show)
|
||||
popup.show()
|
||||
return popup.browser
|
||||
|
||||
def browser_window(self) -> BrowserWindow:
|
||||
parent: QObject = self.parent()
|
||||
while parent is not None:
|
||||
if isinstance(parent, BrowserWindow):
|
||||
return parent
|
||||
|
||||
parent = parent.parent()
|
||||
@@ -1,9 +1,9 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
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
|
||||
|
||||
@@ -14,19 +14,16 @@ class AppWindow(BrowserWindow):
|
||||
app_icon: Optional[QIcon]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.settings = QSettings()
|
||||
super().__init__(**kwargs)
|
||||
self.settings.setParent(self)
|
||||
|
||||
# TODO: Hide from taskbar/group this as part of one big window?
|
||||
flags = Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint | Qt.Window
|
||||
if self.framed:
|
||||
flags |= Qt.CustomizeWindowHint
|
||||
self.setWindowFlags(flags)
|
||||
|
||||
super().__init__(
|
||||
flags=Qt.WindowTitleHint
|
||||
| Qt.WindowCloseButtonHint
|
||||
| Qt.NoDropShadowWindowHint
|
||||
| Qt.WindowStaysOnTopHint,
|
||||
**kwargs
|
||||
)
|
||||
self.pool = QThreadPool(parent=self)
|
||||
self.setWindowTitle(self.app.manifest["appName"])
|
||||
self.snap_to_game()
|
||||
|
||||
self.logger = logging.getLogger(
|
||||
__name__
|
||||
@@ -37,14 +34,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" and sys.platform != "darwin"
|
||||
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 +59,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,10 +1,7 @@
|
||||
from PySide2.QtWebEngine import QtWebEngine
|
||||
|
||||
from .api import Alt1WebChannel, Alt1Api
|
||||
from .profile import WebProfile
|
||||
from .scheme import register as register_scheme
|
||||
|
||||
|
||||
def init():
|
||||
QtWebEngine.initialize()
|
||||
register_scheme()
|
||||
@@ -8,20 +8,6 @@
|
||||
// (even though if discovered, still cannot bypass profiles restriction)
|
||||
const RPC_TOKEN = '%%RPC_TOKEN%%'; // replaced in profile.py
|
||||
|
||||
if (window.parent !== window) {
|
||||
// If running in subframe just use parent's alt1.
|
||||
// Can't run separate instances as we have request ids
|
||||
// that the native part don't know about
|
||||
let wnd = window;
|
||||
while(wnd.parent && wnd.parent !== wnd) {
|
||||
// this is not going to work cross origin
|
||||
wnd = wnd.parent;
|
||||
}
|
||||
|
||||
window.alt1 = wnd.alt1;
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Even though RPC is secure, this is not...
|
||||
let syncChan;
|
||||
let api;
|
||||
@@ -34,25 +20,15 @@
|
||||
});
|
||||
};
|
||||
if (typeof window.qt !== 'undefined') {
|
||||
setup();
|
||||
setup()
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', setup);
|
||||
}
|
||||
});
|
||||
|
||||
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 +38,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++){
|
||||
@@ -93,12 +50,11 @@
|
||||
}
|
||||
|
||||
let lastGameActivity = window.performance.now();
|
||||
let drawCallId = 0;
|
||||
|
||||
window.alt1 = {
|
||||
overlay: {},
|
||||
version: '1.2.0',
|
||||
versionint: 1002000,
|
||||
version: '1.1.1',
|
||||
versionint: 1001001,
|
||||
maxtransfer: 4000000, // also hardcoded in lib...
|
||||
skinName: '',
|
||||
captureMethod: '',
|
||||
@@ -107,6 +63,7 @@
|
||||
permissionOverlay: true,
|
||||
permissionPixel: true,
|
||||
lastWorldHop: 0,
|
||||
currentWorld: -1,
|
||||
rsPing: 0,
|
||||
rsFps: 60,
|
||||
openInfo: '{}',
|
||||
@@ -143,7 +100,7 @@
|
||||
},
|
||||
|
||||
get captureInterval() {
|
||||
if(!syncChan) return 1000;
|
||||
if(!syncChan) return 300;
|
||||
return api.captureInterval;
|
||||
},
|
||||
|
||||
@@ -190,11 +147,6 @@
|
||||
return window.performance.now() - lastGameActivity;
|
||||
},
|
||||
|
||||
get currentWorld() {
|
||||
if(!syncChan) return -1;
|
||||
return api.world || -1;
|
||||
},
|
||||
|
||||
userResize(left, top, right, bottom) {
|
||||
window.resizeTo(right-left, bottom-top);
|
||||
},
|
||||
@@ -208,13 +160,11 @@
|
||||
});
|
||||
},
|
||||
|
||||
openBrowser(url) {
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
// openBrowser(url) {
|
||||
// },
|
||||
|
||||
clearBinds() {
|
||||
window.alt1.clearTooltip();
|
||||
},
|
||||
// clearBinds() {
|
||||
// },
|
||||
|
||||
// registerStatusDaemon(serverUrl, state) {
|
||||
// },
|
||||
@@ -222,15 +172,11 @@
|
||||
// return '';
|
||||
// },
|
||||
|
||||
showNotification(title, message, icon) {
|
||||
// FIXME: Just use HTML5 Notification once QWebEngineNotification is implemented
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.showNotification(title, message, icon);
|
||||
});
|
||||
},
|
||||
// showNotification() {
|
||||
// },
|
||||
|
||||
closeApp() {
|
||||
window.close();
|
||||
window.close()
|
||||
},
|
||||
|
||||
setTooltip(tooltip) {
|
||||
@@ -251,71 +197,42 @@
|
||||
},
|
||||
|
||||
setTitleBarText(text) {
|
||||
|
||||
document.title = text;
|
||||
},
|
||||
|
||||
overLayRect(color, x, y, w, h, time, lineWidth) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayRect(drawCallId++, color, x, y, w, h, time, lineWidth);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLayText(str, color, size, x, y, time) {
|
||||
return alt1.overLayTextEx(str, color, size, x, y, time, '', false, true);
|
||||
console.trace('overLayRect');
|
||||
return false;
|
||||
},
|
||||
overLayTextEx(str, color, size, x, y, time, fontname, centered, shadow) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayTextEx(drawCallId++, str, color, size, x, y, time, fontname, centered, shadow);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLayTextEx');
|
||||
return false;
|
||||
},
|
||||
overLayLine(color, width, x1, y1, x2, y2, time) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayLine(drawCallId++, color, width, x1, y1, x2, y2, time);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLayLine');
|
||||
return false;
|
||||
},
|
||||
overLayImage(x, y, imgstr, imgwidth, time) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayImage(drawCallId++, x, y, imgstr, imgwidth, time);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLayImage');
|
||||
return false;
|
||||
},
|
||||
overLayClearGroup(group) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayClearGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLayClearGroup');
|
||||
},
|
||||
overLaySetGroup(group) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlaySetGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLaySetGroup');
|
||||
},
|
||||
overLayFreezeGroup(group) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayFreezeGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLayFreezeGroup');
|
||||
},
|
||||
overLayContinueGroup(group) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayContinueGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLayContinueGroup');
|
||||
},
|
||||
overLayRefreshGroup(group) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayRefreshGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
console.trace('overLayRefreshGroup');
|
||||
},
|
||||
overLaySetGroupZIndex(group, zIndex) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlaySetGroupZIndex(drawCallId++, group, zIndex);
|
||||
});
|
||||
return true;
|
||||
overLaySetGroupZIndex(groupname, zIndex) {
|
||||
console.trace('overLaySetGroupZIndex');
|
||||
},
|
||||
|
||||
getRegion(x, y, w, h) {
|
||||
@@ -351,31 +268,7 @@
|
||||
// },
|
||||
// bindFindSubImg(id, imgstr, imgwidth, x, y, w, h) {
|
||||
// return '';
|
||||
// },
|
||||
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(){
|
||||
},
|
||||
// }
|
||||
};
|
||||
|
||||
channel.then(function(chan){
|
||||
@@ -394,26 +287,24 @@
|
||||
lastGameActivity = window.performance.now();
|
||||
});
|
||||
|
||||
instance.world_change_signal.connect(function(){
|
||||
alt1.lastWorldHop = new Date().getTime();
|
||||
});
|
||||
|
||||
instance.alt1Signal.connect(function (pos){
|
||||
let mouseX = pos >> 16;
|
||||
let mouseY = pos & 0xFFFF;
|
||||
let mouseRsX = mouseX - instance.gamePositionX;
|
||||
let mouseRsY = mouseY - instance.gamePositionY;
|
||||
let event = {
|
||||
eventName: 'alt1pressed',
|
||||
text: '',
|
||||
mouseAbs: {
|
||||
x: mouseX + instance.gamePositionX,
|
||||
y: mouseY + instance.gamePositionY,
|
||||
},
|
||||
mouseRs: {
|
||||
x: mouseX,
|
||||
y: mouseY,
|
||||
},
|
||||
x: mouseX,
|
||||
y: mouseY,
|
||||
mouseRs: {
|
||||
x: mouseRsX,
|
||||
y: mouseRsY,
|
||||
},
|
||||
x: mouseRsX,
|
||||
y: mouseRsY,
|
||||
rsLinked: true,
|
||||
};
|
||||
emit(event);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import secrets
|
||||
from typing import TYPE_CHECKING, Dict, Callable, List, NamedTuple
|
||||
from typing import TYPE_CHECKING, Dict, Callable, List
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from PIL import Image
|
||||
from PySide2.QtCore import (
|
||||
QObject,
|
||||
Slot,
|
||||
@@ -22,22 +24,44 @@ from PySide2.QtGui import QGuiApplication, QCursor, QScreen
|
||||
from PySide2.QtWebChannel import QWebChannel
|
||||
from PySide2.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlRequestJob
|
||||
|
||||
from runekit.browser.overlay import OverlayApi
|
||||
from runekit.browser.utils import (
|
||||
ApiPermissionDeniedException,
|
||||
image_to_stream,
|
||||
encode_mouse,
|
||||
decode_image,
|
||||
)
|
||||
from runekit.game.instance import ImageType
|
||||
from runekit.ui.tray import tray_icon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from runekit.app.app import App
|
||||
|
||||
TRANSFER_LIMIT = 4_000_000
|
||||
|
||||
class BoundedRegion(NamedTuple):
|
||||
image: ImageType
|
||||
|
||||
class ApiPermissionDeniedException(Exception):
|
||||
required_permission: str
|
||||
|
||||
def __init__(self, required_permission: str):
|
||||
super().__init__(
|
||||
"Permission '%s' is needed for this action".format(required_permission)
|
||||
)
|
||||
self.required_permission = required_permission
|
||||
|
||||
|
||||
def _image_to_stream(image: Image, x=0, y=0, width=None, height=None) -> bytes:
|
||||
assert image.mode == "RGB" or image.mode == "RGBA"
|
||||
|
||||
if width is None:
|
||||
width = image.width
|
||||
if height is None:
|
||||
height = image.height
|
||||
|
||||
if width * height * 4 > TRANSFER_LIMIT:
|
||||
return ""
|
||||
|
||||
if image.mode == "RGB":
|
||||
image = image.convert("RGBA")
|
||||
|
||||
r, g, b, a = image.crop((x, y, x + width, y + height)).split()
|
||||
image = Image.merge("RGBA", (b, g, r, a))
|
||||
|
||||
return base64.b64encode(image.tobytes())
|
||||
|
||||
|
||||
def encode_mouse(x: int, y: int) -> int:
|
||||
return (x << 16) | y
|
||||
|
||||
|
||||
class Alt1Api(QObject):
|
||||
@@ -47,31 +71,20 @@ class Alt1Api(QObject):
|
||||
alt1Signal = Signal(int)
|
||||
|
||||
_screen_info: QRect
|
||||
_bound_regions: List[BoundedRegion]
|
||||
_bound_regions: List
|
||||
_game_active = False
|
||||
_game_position: QRect
|
||||
_game_scaling: float
|
||||
_overlay: OverlayApi
|
||||
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.app = app
|
||||
self._bound_regions = []
|
||||
self._overlay = OverlayApi(self, parent=self)
|
||||
self.logger = logging.getLogger(
|
||||
__name__
|
||||
+ "."
|
||||
+ self.__class__.__name__
|
||||
+ "."
|
||||
+ self.app.manifest["appName"]
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
self._update_screen_info()
|
||||
@@ -79,7 +92,6 @@ class Alt1Api(QObject):
|
||||
self._game_scaling = self.app.game_instance.get_scaling()
|
||||
self._private = Alt1ApiPrivate(self, parent=self)
|
||||
self.app.game_instance.game_activity.connect(self.game_activity_signal)
|
||||
self.app.game_instance.worldChanged.connect(self.world_change_signal)
|
||||
|
||||
poll_timer = QTimer(self)
|
||||
poll_timer.setInterval(250)
|
||||
@@ -101,7 +113,6 @@ class Alt1Api(QObject):
|
||||
self.screen_update_signal.emit()
|
||||
|
||||
screen_update_signal = Signal()
|
||||
|
||||
# endregion
|
||||
|
||||
# region Qt Properties
|
||||
@@ -137,9 +148,9 @@ class Alt1Api(QObject):
|
||||
return 0
|
||||
|
||||
value = QCursor.pos()
|
||||
pos = self._game_position
|
||||
pos = self.app.game_instance.get_position()
|
||||
|
||||
if not pos.contains(value, True) or not self._game_active:
|
||||
if not pos.contains(value, True):
|
||||
# Cursor is out of game
|
||||
return 0
|
||||
|
||||
@@ -192,12 +203,6 @@ class Alt1Api(QObject):
|
||||
gameActive = Property(bool, get_game_active, notify=game_active_change_signal)
|
||||
|
||||
game_activity_signal = Signal()
|
||||
|
||||
def get_world(self):
|
||||
return self.app.game_instance.get_world()
|
||||
|
||||
world_change_signal = Signal()
|
||||
world = Property(bool, get_world, notify=world_change_signal)
|
||||
# endregion
|
||||
|
||||
# region Sync RPC handlers
|
||||
@@ -205,25 +210,14 @@ 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"):
|
||||
return 0
|
||||
|
||||
# Alt1 only support one region
|
||||
bound_data = self.app.game_instance.grab_region(x, y, w, h)
|
||||
self._bound_regions = [BoundedRegion(image=bound_data)]
|
||||
self._bound_regions = [self.app.game_instance.grab_region(x, y, w, h)]
|
||||
return 1
|
||||
|
||||
def bind_get_region(self, id, x, y, w, h):
|
||||
@@ -236,25 +230,10 @@ class Alt1Api(QObject):
|
||||
try:
|
||||
image = self._bound_regions[id - 1]
|
||||
except IndexError:
|
||||
self.logger.warning("bindGetRegion(%d) but image not bound", id)
|
||||
print("no img index %d" % 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")
|
||||
|
||||
if id == 0:
|
||||
return ""
|
||||
|
||||
try:
|
||||
image = self._bound_regions[id - 1]
|
||||
except IndexError:
|
||||
self.logger.warning("bindGetRegionRaw(%d) but image not bound", id)
|
||||
return ""
|
||||
|
||||
return image_to_stream(image.image, x, y, w, h, mode="rgba", ignore_limit=True)
|
||||
return _image_to_stream(image, x, y, w, h)
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -266,13 +245,6 @@ class Alt1Api(QObject):
|
||||
|
||||
self.app.host.notifier.notify(str(text))
|
||||
|
||||
@Slot(str, str, str)
|
||||
def showNotification(self, title, message, icon):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
tray_icon().showMessage(title, message)
|
||||
|
||||
@Slot(str)
|
||||
def identifyAppUrl(self, url):
|
||||
"""Update app manifest from url"""
|
||||
@@ -296,97 +268,6 @@ class Alt1Api(QObject):
|
||||
type_map[type_.toDouble(0)], progress.toDouble(0)
|
||||
)
|
||||
|
||||
@Slot(int, int, int, int, int, int, int, int)
|
||||
def overlayRect(self, call_id, color, x, y, w, h, timeout, line_width):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(
|
||||
call_id, "overlay_rect", color, x, y, w, h, timeout, line_width
|
||||
)
|
||||
|
||||
@Slot(int, int, int, int, int, int, int, int)
|
||||
def overlayLine(self, call_id, color, width, x1, y1, x2, y2, timeout):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(
|
||||
call_id, "overlay_line", color, width, x1, y1, x2, y2, timeout
|
||||
)
|
||||
|
||||
@Slot(int, int, int, str, int, int)
|
||||
def overlayImage(self, call_id, x, y, img_str, img_width, timeout):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
img_bytes = base64.b64decode(img_str)
|
||||
self._overlay.enqueue(
|
||||
call_id, "overlay_image", img_bytes, x, y, img_width, timeout
|
||||
)
|
||||
|
||||
@Slot(int, str, int, int, int, int, int, str, bool, bool)
|
||||
def overlayTextEx(
|
||||
self, call_id, message, color, size, x, y, timeout, font_name, centered, shadow
|
||||
):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(
|
||||
call_id,
|
||||
"overlay_text",
|
||||
message,
|
||||
color,
|
||||
size,
|
||||
x,
|
||||
y,
|
||||
timeout,
|
||||
font_name,
|
||||
centered,
|
||||
shadow,
|
||||
)
|
||||
|
||||
@Slot(int, str)
|
||||
def overlaySetGroup(self, call_id, name):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(call_id, "overlay_set_group", name)
|
||||
|
||||
@Slot(int, str)
|
||||
def overlayClearGroup(self, call_id, name):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(call_id, "overlay_clear_group", name)
|
||||
|
||||
@Slot(int, str)
|
||||
def overlayFreezeGroup(self, call_id, name):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(call_id, "overlay_freeze_group", name)
|
||||
|
||||
@Slot(int, str)
|
||||
def overlayContinueGroup(self, call_id, name):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(call_id, "overlay_continue_group", name)
|
||||
|
||||
@Slot(int, str)
|
||||
def overlayRefreshGroup(self, call_id, name):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(call_id, "overlay_refresh_group", name)
|
||||
|
||||
@Slot(int, str, int)
|
||||
def overlaySetGroupZIndex(self, call_id, group, zIndex):
|
||||
if not self.app.has_permission("overlay"):
|
||||
raise ApiPermissionDeniedException("overlay")
|
||||
|
||||
self._overlay.enqueue(call_id, "overlay_set_group_z", group, zIndex)
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import sys
|
||||
import collections
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import List, Dict, Tuple, Optional, TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import QObject, QTimer, Qt
|
||||
from PySide2.QtGui import QFont, QPen, QImage, QPixmap
|
||||
from PySide2.QtWidgets import (
|
||||
QGraphicsItem,
|
||||
QGraphicsDropShadowEffect,
|
||||
QGraphicsTextItem,
|
||||
QGraphicsRectItem,
|
||||
QGraphicsItemGroup,
|
||||
QGraphicsLineItem,
|
||||
QGraphicsPixmapItem,
|
||||
)
|
||||
|
||||
from .utils import decode_color
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .api import Alt1Api
|
||||
|
||||
|
||||
def barrier(f):
|
||||
"""Mark f as sync barrier - all preceeding calls must be finished.
|
||||
Only affect calls from scheduler"""
|
||||
f.barrier = True
|
||||
return f
|
||||
|
||||
|
||||
def ensure_overlay(f):
|
||||
def out(self: "OverlayApi", *args, **kwargs):
|
||||
if not self.overlay_area:
|
||||
return
|
||||
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
return functools.update_wrapper(out, f)
|
||||
|
||||
|
||||
class OverlayApi(QObject):
|
||||
# TODO: I think this could be implemented by QGraphicsItemGroup, maybe even more performant
|
||||
current_group = ""
|
||||
groups: Dict[str, List[QGraphicsItem]]
|
||||
frozen_group: Dict[str, QGraphicsItemGroup]
|
||||
queue: List[Tuple[int, str, List]]
|
||||
last_call_id: Optional[int] = None
|
||||
overlay_area: Optional[QGraphicsItem] = None
|
||||
|
||||
def __init__(self, base_api: "Alt1Api", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.api = base_api
|
||||
self.reset()
|
||||
|
||||
try:
|
||||
self.overlay_area = self.api.app.game_instance.get_overlay_area()
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
@ensure_overlay
|
||||
def enqueue(self, call_id, command, *args):
|
||||
# XXX: command is NOT validated. Must be from secure source
|
||||
assert command[0] != "_"
|
||||
|
||||
if call_id == 0:
|
||||
self.logger.info("Call ID reset")
|
||||
self.reset()
|
||||
|
||||
self.queue.append((call_id, command, args))
|
||||
self.queue.sort(key=lambda x: x[0])
|
||||
# self.logger.debug("%d %s %s", call_id, command, repr(args))
|
||||
|
||||
QTimer.singleShot(0, self.process_queue)
|
||||
|
||||
def process_queue(self):
|
||||
while len(self.queue) > 0:
|
||||
head = self.queue[0]
|
||||
f = getattr(self, head[1])
|
||||
if (
|
||||
hasattr(f, "barrier")
|
||||
and self.last_call_id is not None
|
||||
and head[0] != self.last_call_id + 1
|
||||
):
|
||||
return
|
||||
|
||||
head = self.queue.pop(0)
|
||||
self.last_call_id = head[0]
|
||||
try:
|
||||
f(*head[2])
|
||||
except:
|
||||
self.logger.error(
|
||||
"API call exception #%d %s(%s)",
|
||||
head[0],
|
||||
head[1],
|
||||
repr(head[2]),
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def _finalize_gfx(
|
||||
self, gfx: QGraphicsItem, timeout: int, group: Optional[str] = None
|
||||
):
|
||||
timeout = min(20000, max(timeout, 1))
|
||||
|
||||
if group is None:
|
||||
group = self.current_group
|
||||
|
||||
if group in self.frozen_group:
|
||||
self.frozen_group[group].addToGroup(gfx)
|
||||
else:
|
||||
self.groups[group].append(gfx)
|
||||
gfx.setParentItem(self.overlay_area)
|
||||
|
||||
def hide():
|
||||
try:
|
||||
gfx.scene().removeItem(gfx)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.groups[group].remove(gfx)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
|
||||
QTimer.singleShot(timeout, hide)
|
||||
|
||||
def reset(self):
|
||||
if hasattr(self, "groups"):
|
||||
for item in self.groups.values():
|
||||
item.scene().removeItem(item)
|
||||
|
||||
self.groups = collections.defaultdict(list)
|
||||
self.frozen_group = {}
|
||||
self.queue = []
|
||||
self.last_call_id = None
|
||||
|
||||
@barrier
|
||||
@ensure_overlay
|
||||
def overlay_set_group(self, name: str):
|
||||
self.current_group = name
|
||||
|
||||
@barrier
|
||||
@ensure_overlay
|
||||
def overlay_clear_group(self, name: str):
|
||||
for item in self.groups.get(name, []):
|
||||
item.scene().removeItem(item)
|
||||
|
||||
try:
|
||||
del self.groups[name]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@barrier
|
||||
@ensure_overlay
|
||||
def overlay_freeze_group(self, name: str):
|
||||
if name in self.frozen_group:
|
||||
return
|
||||
|
||||
self.frozen_group[name] = QGraphicsItemGroup()
|
||||
|
||||
@barrier
|
||||
@ensure_overlay
|
||||
def overlay_continue_group(self, name: str):
|
||||
if name not in self.frozen_group:
|
||||
return
|
||||
|
||||
gfx = self.frozen_group[name]
|
||||
del self.frozen_group[name]
|
||||
self._finalize_gfx(gfx, 20000, group=name)
|
||||
|
||||
@barrier
|
||||
@ensure_overlay
|
||||
def overlay_refresh_group(self, name: str):
|
||||
if name not in self.frozen_group:
|
||||
return
|
||||
|
||||
self.overlay_continue_group(name)
|
||||
self.overlay_freeze_group(name)
|
||||
|
||||
@ensure_overlay
|
||||
def overlay_rect(
|
||||
self, color: int, x: int, y: int, w: int, h: int, timeout: int, line_width: int
|
||||
):
|
||||
pen = QPen(decode_color(color))
|
||||
pen.setWidthF(max(1.0, line_width / 10))
|
||||
|
||||
gfx = QGraphicsRectItem(x, y, w, h)
|
||||
gfx.setPen(pen)
|
||||
|
||||
self._finalize_gfx(gfx, timeout)
|
||||
|
||||
@ensure_overlay
|
||||
def overlay_line(
|
||||
self,
|
||||
color: int,
|
||||
line_width: int,
|
||||
x1: int,
|
||||
y1: int,
|
||||
x2: int,
|
||||
y2: int,
|
||||
timeout: int,
|
||||
):
|
||||
pen = QPen(decode_color(color))
|
||||
pen.setWidthF(max(1.0, line_width / 10))
|
||||
|
||||
gfx = QGraphicsLineItem(x1, y1, x2, y2)
|
||||
gfx.setPen(pen)
|
||||
|
||||
self._finalize_gfx(gfx, timeout)
|
||||
|
||||
@ensure_overlay
|
||||
def overlay_text(
|
||||
self,
|
||||
message: str,
|
||||
color: int,
|
||||
size: int,
|
||||
x: int,
|
||||
y: int,
|
||||
timeout: int,
|
||||
font_name: str,
|
||||
centered: bool,
|
||||
shadow: bool,
|
||||
):
|
||||
gfx = QGraphicsTextItem(message)
|
||||
gfx.setDefaultTextColor(decode_color(color))
|
||||
|
||||
if font_name == "" and sys.platform == "darwin":
|
||||
# Don't use Helvetica on Mac
|
||||
font_name = "Verdana"
|
||||
|
||||
font = QFont(font_name, min(50, size))
|
||||
font.setStyleHint(QFont.SansSerif)
|
||||
gfx.setFont(font)
|
||||
|
||||
if shadow:
|
||||
effect = QGraphicsDropShadowEffect(gfx)
|
||||
effect.setBlurRadius(0)
|
||||
effect.setColor(Qt.GlobalColor.black)
|
||||
effect.setOffset(1, 1)
|
||||
gfx.setGraphicsEffect(effect)
|
||||
|
||||
if centered:
|
||||
# The provided x, y is at the center of the text
|
||||
bound = gfx.boundingRect()
|
||||
gfx.setPos(x - (bound.width() / 2), y - (bound.height() / 2))
|
||||
else:
|
||||
gfx.setPos(x, y)
|
||||
|
||||
self._finalize_gfx(gfx, timeout)
|
||||
|
||||
@ensure_overlay
|
||||
def overlay_image(self, img: bytes, x: int, y: int, width: int, timeout: int):
|
||||
img = self.get_qimage(img, width)
|
||||
|
||||
gfx = QGraphicsPixmapItem(QPixmap.fromImage(img))
|
||||
gfx.setPos(x, y)
|
||||
|
||||
self._finalize_gfx(gfx, timeout)
|
||||
|
||||
@ensure_overlay
|
||||
def overlay_set_group_z(self, name: str, z_index: int):
|
||||
for item in self.groups.get(name, []):
|
||||
item.setZValue(z_index)
|
||||
|
||||
@functools.lru_cache(100)
|
||||
def get_qimage(self, img: bytes, width: int):
|
||||
height = len(img) / width / 4
|
||||
return QImage(img, width, height, QImage.Format_ARGB32)
|
||||
@@ -1,12 +1,9 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import QFile, QIODevice
|
||||
from PySide2.QtWebEngineWidgets import (
|
||||
QWebEngineProfile,
|
||||
QWebEngineScript,
|
||||
QWebEngineSettings,
|
||||
)
|
||||
from PySide2.QtWebEngineWidgets import QWebEngineProfile, QWebEngineScript
|
||||
|
||||
from runekit.browser.api import RuneKitSchemeHandler
|
||||
from runekit.browser.scheme import RuneKitScheme
|
||||
@@ -14,23 +11,25 @@ from runekit.browser.scheme import RuneKitScheme
|
||||
if TYPE_CHECKING:
|
||||
from runekit.app import App
|
||||
|
||||
script_file = Path(__file__).parent / "alt1.js"
|
||||
|
||||
with script_file.open() as fp:
|
||||
src = fp.read()
|
||||
|
||||
_added_qwebchannel = False
|
||||
|
||||
|
||||
class WebProfile(QWebEngineProfile):
|
||||
rpc_secret: str
|
||||
app: "App"
|
||||
|
||||
def __init__(self, app=None, parent=None):
|
||||
super().__init__("app", parent=parent)
|
||||
# TODO: Persist
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.rpc_secret = secrets.token_urlsafe(64)
|
||||
self._insert_scheme_handlers()
|
||||
self._insert_alt1_api()
|
||||
self.settings().setAttribute(
|
||||
QWebEngineSettings.PlaybackRequiresUserGesture, False
|
||||
)
|
||||
self.settings().setAttribute(QWebEngineSettings.PdfViewerEnabled, False)
|
||||
self.settings().setAttribute(QWebEngineSettings.ScreenCaptureEnabled, False)
|
||||
self.settings().setAttribute(QWebEngineSettings.AutoLoadIconsForPage, False)
|
||||
|
||||
def _insert_scheme_handlers(self):
|
||||
self.installUrlSchemeHandler(
|
||||
@@ -43,25 +42,23 @@ class WebProfile(QWebEngineProfile):
|
||||
)
|
||||
|
||||
def _insert_alt1_api(self):
|
||||
qwc_file = QFile(":/qtwebchannel/qwebchannel.js", parent=self)
|
||||
if not qwc_file.open(QIODevice.ReadOnly):
|
||||
raise IOError
|
||||
src = bytes(qwc_file.readAll()).decode("utf8")
|
||||
qwc_file.close()
|
||||
global _added_qwebchannel, src
|
||||
|
||||
src += "\n;;\n"
|
||||
if not _added_qwebchannel:
|
||||
qwc_file = QFile(":/qtwebchannel/qwebchannel.js", parent=self)
|
||||
if not qwc_file.open(QIODevice.ReadOnly):
|
||||
raise IOError
|
||||
qwc_src = bytes(qwc_file.readAll()).decode("utf8")
|
||||
qwc_file.close()
|
||||
|
||||
alt1_file = QFile(":/runekit/browser/alt1.js", parent=self)
|
||||
if not alt1_file.open(QIODevice.ReadOnly):
|
||||
raise IOError
|
||||
src += bytes(alt1_file.readAll()).decode("utf8")
|
||||
alt1_file.close()
|
||||
src = qwc_src + "\n;;\n" + src
|
||||
|
||||
_added_qwebchannel = True
|
||||
|
||||
script = QWebEngineScript()
|
||||
script.setName("alt1.js")
|
||||
script.setWorldId(QWebEngineScript.MainWorld)
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
|
||||
script.setSourceCode(src.replace("%%RPC_TOKEN%%", self.rpc_secret))
|
||||
script.setRunsOnSubFrames(True)
|
||||
|
||||
self.scripts().insert(script)
|
||||
@@ -1,107 +0,0 @@
|
||||
import base64
|
||||
from typing import TypeVar
|
||||
|
||||
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
|
||||
|
||||
|
||||
class ApiPermissionDeniedException(Exception):
|
||||
required_permission: str
|
||||
|
||||
def __init__(self, required_permission: str):
|
||||
super().__init__(
|
||||
"Permission '%s' is needed for this action".format(required_permission)
|
||||
)
|
||||
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",
|
||||
ignore_limit=False,
|
||||
) -> bytes:
|
||||
if isinstance(image, np.ndarray):
|
||||
out = np_crop(image, x, y, width, height)
|
||||
out = ensure_image(out, mode).tobytes()
|
||||
else:
|
||||
assert image.mode == "RGBA"
|
||||
|
||||
if width is None:
|
||||
width = image.width
|
||||
if height is None:
|
||||
height = image.height
|
||||
|
||||
if not ignore_limit and width * height * 4 > TRANSFER_LIMIT:
|
||||
return ""
|
||||
|
||||
image = image.crop((x, y, x + width, y + height))
|
||||
|
||||
out = ensure_image(image, mode).tobytes()
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def encode_mouse(x: int, y: int) -> int:
|
||||
return (x << 16) | y
|
||||
|
||||
|
||||
def decode_color(color: int) -> QColor:
|
||||
r = (color >> 16) & 0xFF
|
||||
g = (color >> 8) & 0xFF
|
||||
b = (color >> 0) & 0xFF
|
||||
a = (color >> 24) & 0xFF
|
||||
return QColor.fromRgb(r, g, b, a)
|
||||
|
||||
|
||||
def decode_image(img: str, width: int) -> np.ndarray:
|
||||
img = base64.b64decode(img)
|
||||
img = np.frombuffer(img, "<B")
|
||||
img.shape = (-1, width, 4)
|
||||
return img
|
||||
@@ -1,24 +1,18 @@
|
||||
import abc
|
||||
import dataclasses
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
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 PIL import Image
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import GameManager
|
||||
|
||||
PROGRESS_TYPE = Literal["RESET", "IN_PROGRESS", "ERROR", "LOADING", "PAUSED"]
|
||||
ImageType = Union[np.ndarray, Image.Image]
|
||||
|
||||
|
||||
class GameInstance(QObject):
|
||||
refresh_rate = 1000
|
||||
refresh_rate = 300
|
||||
manager: "GameManager"
|
||||
_last_game_activity: float = 0
|
||||
|
||||
@@ -40,7 +34,6 @@ class GameInstance(QObject):
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_position(self) -> QRect:
|
||||
"""Return the game window position, without space used by window decoration"""
|
||||
...
|
||||
|
||||
positionChanged = Signal(QRect)
|
||||
@@ -61,33 +54,17 @@ class GameInstance(QObject):
|
||||
focused = Property(bool, is_focused, notify=focusChanged)
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_world(self) -> Optional[int]:
|
||||
...
|
||||
|
||||
worldChanged = Signal(int)
|
||||
world = Property(int, get_world, notify=worldChanged)
|
||||
|
||||
@abc.abstractmethod
|
||||
def grab_game(self) -> ImageType:
|
||||
def grab_game(self) -> Image:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def grab_desktop(self, x: int, y: int, w: int, h: int) -> ImageType:
|
||||
def grab_desktop(self, x: int, y: int, w: int, h: int) -> Image:
|
||||
...
|
||||
|
||||
def grab_region(self, x: int, y: int, w: int, h: int) -> ImageType:
|
||||
def grab_region(self, x: int, y: int, w: int, h: int) -> Image:
|
||||
image = self.grab_game()
|
||||
return image.crop((x, y, x + w, y + h))
|
||||
|
||||
if isinstance(image, np.ndarray):
|
||||
return np_crop(image, x, y, w, h)
|
||||
else:
|
||||
return image.crop((x, y, x + w, y + h))
|
||||
|
||||
def set_taskbar_progress(self, type_: PROGRESS_TYPE, progress: float):
|
||||
pass
|
||||
|
||||
def embed_window(self, window: QWindow):
|
||||
pass
|
||||
|
||||
def get_overlay_area(self) -> QGraphicsItem:
|
||||
raise NotImplementedError
|
||||
@abc.abstractmethod
|
||||
def set_taskbar_progress(self, type, progress):
|
||||
...
|
||||
@@ -1,5 +1,5 @@
|
||||
import abc
|
||||
from typing import List, Union
|
||||
from typing import List
|
||||
|
||||
from PySide2.QtCore import QObject, Property, Signal
|
||||
|
||||
@@ -12,15 +12,7 @@ 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)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the GameManager and any GameInstances"""
|
||||
pass
|
||||
@@ -1,108 +0,0 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Callable, Tuple, Dict
|
||||
|
||||
import numpy as np
|
||||
from PySide2.QtCore import Qt, QRect, QTimer
|
||||
from PySide2.QtGui import QGuiApplication, QPen
|
||||
from PySide2.QtWidgets import (
|
||||
QMainWindow,
|
||||
QGraphicsView,
|
||||
QGraphicsScene,
|
||||
QGraphicsItem,
|
||||
QGraphicsRectItem,
|
||||
)
|
||||
|
||||
from .qt import qpixmap_to_np
|
||||
from ..image import is_color_percent_gte
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .instance import GameInstance
|
||||
|
||||
|
||||
class DesktopWideOverlay(QMainWindow):
|
||||
_instances: Dict[int, QGraphicsItem]
|
||||
view: QGraphicsView
|
||||
scene: QGraphicsScene
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
flags=Qt.Widget
|
||||
| Qt.FramelessWindowHint
|
||||
| Qt.BypassWindowManagerHint
|
||||
| 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)
|
||||
|
||||
for screen in QGuiApplication.screens():
|
||||
# TODO: Handle screen change
|
||||
geom = screen.virtualGeometry()
|
||||
virtual_screen = virtual_screen.united(geom)
|
||||
|
||||
self.scene = QGraphicsScene(
|
||||
0, 0, virtual_screen.width(), virtual_screen.height(), parent=self
|
||||
)
|
||||
|
||||
self.view = QGraphicsView(self.scene, self)
|
||||
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.view.setStyleSheet("background: transparent")
|
||||
self.view.setGeometry(0, 0, virtual_screen.width(), virtual_screen.height())
|
||||
self.view.setInteractive(False)
|
||||
|
||||
self.transparent_pen = QPen()
|
||||
self.transparent_pen.setBrush(Qt.NoBrush)
|
||||
|
||||
self.setGeometry(virtual_screen)
|
||||
|
||||
def add_instance(
|
||||
self, instance: "GameInstance"
|
||||
) -> Tuple[QGraphicsItem, Callable[[], None]]:
|
||||
"""Add instance to manage, return a disconnect function and the canvas"""
|
||||
positionChanged = lambda rect: self.on_instance_moved(instance, rect)
|
||||
instance.positionChanged.connect(positionChanged)
|
||||
|
||||
focusChanged = lambda focus: self.on_instance_focus_change(instance, focus)
|
||||
instance.focusChanged.connect(focusChanged)
|
||||
|
||||
instance_pos = instance.get_position()
|
||||
gfx = QGraphicsRectItem(rect=instance_pos)
|
||||
gfx.setPen(self.transparent_pen)
|
||||
gfx.setPos(instance_pos.x(), instance_pos.y())
|
||||
self.scene.addItem(gfx)
|
||||
self._instances[instance.wid] = gfx
|
||||
|
||||
def disconnect():
|
||||
gfx.hide()
|
||||
self.scene.removeItem(gfx)
|
||||
instance.positionChanged.disconnect(positionChanged)
|
||||
instance.focusChanged.disconnect(focusChanged)
|
||||
|
||||
return gfx, disconnect
|
||||
|
||||
def on_instance_focus_change(self, instance, focus):
|
||||
# self._instances[instance.wid].setVisible(focus)
|
||||
pass
|
||||
|
||||
def on_instance_moved(self, instance, pos: QRect):
|
||||
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)
|
||||
if is_color_percent_gte(image, color=[0, 0, 0], percent=0.95):
|
||||
self.logger.warning("Detected black screen condition. Disabling overlay")
|
||||
self.hide()
|
||||
@@ -1,94 +0,0 @@
|
||||
import re
|
||||
import socket
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
import psutil
|
||||
from PySide2.QtCore import QTimer, Slot
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
mode = None
|
||||
|
||||
MODE_PROCESS = 1
|
||||
MODE_GLOBAL = 2
|
||||
|
||||
|
||||
class PsUtilBaseMixin:
|
||||
pid: int
|
||||
|
||||
|
||||
class PsUtilNetStat(PsUtilBaseMixin):
|
||||
__cached_rdns: dict[str, str]
|
||||
__last_world = None
|
||||
world_regex = re.compile(r"^world([0-9]+)\.runescape\.com$")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__cached_rdns = {}
|
||||
self.__world_timer = QTimer(self)
|
||||
self.__world_timer.start(60_000)
|
||||
self.__world_timer.timeout.connect(self.__update_world)
|
||||
|
||||
def get_world(self):
|
||||
if self.__last_world is None:
|
||||
self.__last_world = self.fetch_world()
|
||||
|
||||
return self.__last_world
|
||||
|
||||
def fetch_world(self) -> Optional[int]:
|
||||
try:
|
||||
addrs = self.__get_connections()
|
||||
except psutil.AccessDenied:
|
||||
logger.warning("Cannot get connections", exc_info=True)
|
||||
return None
|
||||
|
||||
for conn in addrs:
|
||||
if conn.status != psutil.CONN_ESTABLISHED:
|
||||
continue
|
||||
|
||||
dns_name = self._rdns(conn.raddr.ip)
|
||||
matched_name = self.world_regex.match(dns_name)
|
||||
if not matched_name:
|
||||
continue
|
||||
|
||||
world = int(matched_name.group(1))
|
||||
return world
|
||||
|
||||
def __get_connections(self):
|
||||
global mode
|
||||
if mode is not None:
|
||||
return self.__get_connections_mode(mode)
|
||||
|
||||
last_exc = None
|
||||
for method in [MODE_PROCESS, MODE_GLOBAL]:
|
||||
try:
|
||||
out = self.__get_connections_mode(method)
|
||||
mode = method
|
||||
return out
|
||||
except psutil.AccessDenied as e:
|
||||
last_exc = e
|
||||
|
||||
raise last_exc
|
||||
|
||||
def __get_connections_mode(self, mode_: int):
|
||||
if mode_ is MODE_PROCESS:
|
||||
return psutil.Process(self.pid).connections()
|
||||
elif mode_ is MODE_GLOBAL:
|
||||
return [item for item in psutil.net_connections() if item.pid == self.pid]
|
||||
else:
|
||||
raise ValueError(f"Unknown mode {mode_}")
|
||||
|
||||
@Slot()
|
||||
def __update_world(self):
|
||||
new_world = self.fetch_world()
|
||||
if new_world != self.__last_world:
|
||||
logger.info("World hopped from %d to %d", self.__last_world, new_world)
|
||||
self.__last_world = new_world
|
||||
self.worldChanged.emit(new_world)
|
||||
|
||||
def _rdns(self, ip: str) -> Optional[str]:
|
||||
if ip in self.__cached_rdns:
|
||||
return self.__cached_rdns[ip]
|
||||
|
||||
self.__cached_rdns[ip] = socket.getnameinfo((ip, 0), 0)[0]
|
||||
return self.__cached_rdns[ip]
|
||||
@@ -1,37 +1,13 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from PySide2.QtCore import QBuffer, QIODevice
|
||||
from PySide2.QtGui import QGuiApplication, QWindow, QPixmap
|
||||
import cv2
|
||||
|
||||
from runekit.image.np_utils import np_save_image
|
||||
from PIL import Image
|
||||
from PySide2.QtGui import QGuiApplication, QWindow
|
||||
|
||||
_debug_dump_file = False
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def qpixmap_to_np(im: QPixmap) -> np.ndarray:
|
||||
# from PIL.ImageQt.fromqimage
|
||||
buffer = QBuffer()
|
||||
buffer.open(QIODevice.ReadWrite)
|
||||
if im.hasAlphaChannel():
|
||||
im.save(buffer, "png")
|
||||
else:
|
||||
im.save(buffer, "ppm")
|
||||
|
||||
npbuf = np.frombuffer(buffer.data(), "<B")
|
||||
buffer.close()
|
||||
|
||||
out = cv2.imdecode(npbuf, cv2.IMREAD_UNCHANGED)
|
||||
if out.shape[2] == 3:
|
||||
# Add alpha channel
|
||||
out = np.pad(out, ((0, 0), (0, 0), (0, 1)), constant_values=0xFF)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class QtBaseMixin:
|
||||
qwindow: QWindow
|
||||
|
||||
@@ -42,34 +18,29 @@ class QtGrabMixin(QtBaseMixin):
|
||||
__game_last_grab = 0.0
|
||||
__game_last_image = None
|
||||
|
||||
def grab_game(self) -> np.ndarray:
|
||||
def grab_game(self) -> Image:
|
||||
if (time.monotonic() - self.__game_last_grab) * 1000 < self.refresh_rate:
|
||||
return self.__game_last_image
|
||||
|
||||
screen = QGuiApplication.primaryScreen()
|
||||
pixmap = screen.grabWindow(self.qwindow.winId())
|
||||
image = qpixmap_to_np(pixmap)
|
||||
image = Image.fromqpixmap(pixmap)
|
||||
|
||||
if _debug_dump_file:
|
||||
logger.debug("dumping file")
|
||||
np_save_image(image, "/tmp/qtshot.bmp")
|
||||
pixmap.save("/tmp/qtshot.bmp", None, 100)
|
||||
|
||||
self.__game_last_image = image
|
||||
self.__game_last_grab = time.monotonic()
|
||||
return self.__game_last_image
|
||||
|
||||
def grab_desktop(self, x, y, w, h) -> np.ndarray:
|
||||
def grab_desktop(self, x, y, w, h) -> Image:
|
||||
screen = QGuiApplication.primaryScreen()
|
||||
pixmap = screen.grabWindow(0, x, y, w, h)
|
||||
image = qpixmap_to_np(pixmap)
|
||||
image = Image.fromqpixmap(pixmap)
|
||||
|
||||
if _debug_dump_file:
|
||||
logger.debug("dumping file")
|
||||
np_save_image(image, "/tmp/qtshot.bmp")
|
||||
pixmap.save("/tmp/qtshot.bmp", None, 100)
|
||||
|
||||
return image
|
||||
|
||||
|
||||
class QtEmbedMixin(QtBaseMixin):
|
||||
def embed_window(self, window: QWindow):
|
||||
window.setParent(self.qwindow)
|
||||
@@ -1,7 +1,6 @@
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import Quartz
|
||||
import objc
|
||||
@@ -9,13 +8,8 @@ from PIL import Image
|
||||
from PySide2.QtCore import QRect
|
||||
from PySide2.QtGui import QGuiApplication
|
||||
import ApplicationServices
|
||||
from PySide2.QtWidgets import QGraphicsItem
|
||||
|
||||
from ..instance import GameInstance
|
||||
from ..psutil_mixins import PsUtilNetStat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import QuartzGameManager
|
||||
|
||||
_debug_dump_file = False
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -77,14 +71,13 @@ def on_ax_event(observer, element, notification, ptr):
|
||||
self.logger.warning("Got unknown AX event %s", notification)
|
||||
|
||||
|
||||
class QuartzGameInstance(PsUtilNetStat, GameInstance):
|
||||
class QuartzGameInstance(GameInstance):
|
||||
_is_active = False
|
||||
overlay: QGraphicsItem
|
||||
|
||||
__game_last_grab = 0.0
|
||||
__game_last_image = None
|
||||
|
||||
def __init__(self, manager: "QuartzGameManager", wid, pid, **kwargs):
|
||||
def __init__(self, manager, wid, pid, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.manager = manager
|
||||
self.wid = wid
|
||||
@@ -94,7 +87,6 @@ class QuartzGameInstance(PsUtilNetStat, GameInstance):
|
||||
|
||||
self._setup_observer()
|
||||
self.update_is_active()
|
||||
self.overlay, self._overlay_disconnect = self.manager.overlay.add_instance(self)
|
||||
|
||||
def _setup_observer(self):
|
||||
self._ax_element = ApplicationServices.AXUIElementCreateApplication(self.pid)
|
||||
@@ -131,7 +123,6 @@ class QuartzGameInstance(PsUtilNetStat, GameInstance):
|
||||
)
|
||||
|
||||
objc.context.unregister(self)
|
||||
self._overlay_disconnect()
|
||||
|
||||
def get_position(self) -> QRect:
|
||||
# The docs say this API is expensive...
|
||||
@@ -164,10 +155,6 @@ class QuartzGameInstance(PsUtilNetStat, GameInstance):
|
||||
[self.wid],
|
||||
Quartz.kCGWindowImageBoundsIgnoreFraming,
|
||||
)
|
||||
if not imgref:
|
||||
self.manager.request_accessibility_popup.emit()
|
||||
raise NoCapturePermission
|
||||
|
||||
out = cgimageref_to_image(imgref)
|
||||
scale = self.get_scaling()
|
||||
if scale > 1:
|
||||
@@ -189,8 +176,8 @@ class QuartzGameInstance(PsUtilNetStat, GameInstance):
|
||||
out = cgimageref_to_image(imgref)
|
||||
return out.resize((w, h), Image.NEAREST)
|
||||
|
||||
def get_overlay_area(self) -> QGraphicsItem:
|
||||
return self.overlay
|
||||
def set_taskbar_progress(self, type, progress):
|
||||
pass
|
||||
|
||||
|
||||
class AXAPIError(Exception):
|
||||
@@ -201,7 +188,6 @@ class AXAPIError(Exception):
|
||||
ApplicationServices.kAXErrorNotificationAlreadyRegistered: "The notification has already been registered",
|
||||
ApplicationServices.kAXErrorCannotComplete: "The function cannot complete because messaging has failed in some way.",
|
||||
ApplicationServices.kAXErrorFailure: "There is some sort of system memory failure.",
|
||||
ApplicationServices.kAXErrorAPIDisabled: "Assistive applications are not enabled in System Preferences.",
|
||||
}
|
||||
|
||||
def __init__(self, code):
|
||||
@@ -209,8 +195,3 @@ class AXAPIError(Exception):
|
||||
raise ValueError("Success")
|
||||
|
||||
super().__init__(self.mapping.get(code, f"API Error: {code}"))
|
||||
|
||||
|
||||
class NoCapturePermission(Exception):
|
||||
def __init__(self):
|
||||
super().__init__("Screen Recording permission is not allowed")
|
||||
@@ -1,15 +1,12 @@
|
||||
import time
|
||||
from functools import reduce
|
||||
from typing import List, Dict, Optional, Union
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
import Quartz
|
||||
import ApplicationServices
|
||||
from PySide2.QtCore import QTimer, Signal, Slot
|
||||
from PySide2.QtCore import QTimer
|
||||
from PySide2.QtGui import QDesktopServices
|
||||
from PySide2.QtWidgets import QMessageBox
|
||||
|
||||
from .instance import QuartzGameInstance
|
||||
from runekit.game.overlay import DesktopWideOverlay
|
||||
from ..instance import GameInstance
|
||||
from ..manager import GameManager
|
||||
|
||||
@@ -18,26 +15,12 @@ has_prompted_accessibility = False
|
||||
|
||||
class QuartzGameManager(GameManager):
|
||||
_instances: Dict[int, GameInstance]
|
||||
overlay: DesktopWideOverlay
|
||||
|
||||
request_accessibility_popup = Signal()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._instances = {}
|
||||
self.request_accessibility_popup.connect(self.accessibility_popup)
|
||||
self._setup_tap()
|
||||
|
||||
ApplicationServices.AXIsProcessTrustedWithOptions(
|
||||
{
|
||||
ApplicationServices.kAXTrustedCheckOptionPrompt: True,
|
||||
}
|
||||
)
|
||||
while not ApplicationServices.AXIsProcessTrusted():
|
||||
time.sleep(0.1)
|
||||
|
||||
self._setup_overlay()
|
||||
|
||||
def _setup_tap(self):
|
||||
events = [
|
||||
Quartz.kCGEventLeftMouseDown,
|
||||
@@ -59,24 +42,6 @@ class QuartzGameManager(GameManager):
|
||||
Quartz.CFRunLoopGetCurrent(), source, Quartz.kCFRunLoopCommonModes
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
self.overlay.hide()
|
||||
self.overlay.deleteLater()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def get_instances(self) -> List[GameInstance]:
|
||||
windows = Quartz.CGWindowListCopyWindowInfo(
|
||||
Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID
|
||||
@@ -93,12 +58,6 @@ class QuartzGameManager(GameManager):
|
||||
|
||||
return list(self._instances.values())
|
||||
|
||||
def get_active_instance(self) -> Union[GameInstance, None]:
|
||||
if not self._instances:
|
||||
return None
|
||||
|
||||
return list(self._instances.values())[0]
|
||||
|
||||
def get_instance_by_pid(self, pid: int) -> Optional[QuartzGameInstance]:
|
||||
for instance in self._instances.values():
|
||||
if instance.pid == pid:
|
||||
@@ -119,9 +78,8 @@ class QuartzGameManager(GameManager):
|
||||
instance = self.get_instance_by_pid(front_app.processIdentifier())
|
||||
else:
|
||||
instance = self._instances.get(nsevent.windowNumber())
|
||||
|
||||
if not instance:
|
||||
return event
|
||||
if not instance:
|
||||
return event
|
||||
|
||||
# Check for cmd1
|
||||
if nsevent.type() == Quartz.NSEventTypeKeyDown:
|
||||
@@ -136,22 +94,25 @@ class QuartzGameManager(GameManager):
|
||||
|
||||
return event
|
||||
|
||||
@Slot()
|
||||
def accessibility_popup(self):
|
||||
global has_prompted_accessibility
|
||||
if has_prompted_accessibility:
|
||||
return
|
||||
|
||||
# TODO: Check AXIsProcessTrusted?
|
||||
has_prompted_accessibility = True
|
||||
msgbox = QMessageBox(
|
||||
QMessageBox.Warning,
|
||||
"Permission required",
|
||||
"RuneKit needs Screen Recording permission\n\nOpen System Preferences > Security > Privacy > Screen Recording to allow this",
|
||||
"RuneKit needs Accessibility Access for global hotkey and game activity monitoring\n\nOpen System Preferences > Security > Privacy > Accessibility to allow this",
|
||||
QMessageBox.Open | QMessageBox.Ignore,
|
||||
)
|
||||
button = msgbox.exec()
|
||||
|
||||
if button == QMessageBox.Open:
|
||||
QDesktopServices.openUrl(
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Screen Recording"
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
|
||||
)
|
||||
|
||||
# FIXME: Closing this dialog will close the app, idk why
|
||||
# I think Qt recognize the dialog as the last app window (which is untrue)
|
||||
@@ -1,180 +1,43 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import xcffib.composite
|
||||
import xcffib.xproto
|
||||
from PySide2.QtCore import QRect, Signal, Slot
|
||||
from PySide2.QtCore import QRect
|
||||
from PySide2.QtGui import QWindow, QGuiApplication
|
||||
from PySide2.QtWidgets import QGraphicsItem
|
||||
from Xlib.xobject.drawable import Window
|
||||
|
||||
from runekit.game.instance import GameInstance
|
||||
from runekit.game.psutil_mixins import PsUtilNetStat
|
||||
from runekit.game.qt import QtGrabMixin, QtEmbedMixin
|
||||
from .ximage import zpixmap_shm_to_image
|
||||
from runekit.game.qt import QtGrabMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import X11GameManager
|
||||
|
||||
|
||||
class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
|
||||
wid: int
|
||||
refresh_rate = 100
|
||||
class X11GameInstance(QtGrabMixin, GameInstance):
|
||||
xwindow: Window
|
||||
|
||||
manager: "X11GameManager"
|
||||
overlay: QGraphicsItem
|
||||
|
||||
game_last_grab = 0.0
|
||||
game_last_image = None
|
||||
embedded_windows: List[QWindow]
|
||||
cached_position = None
|
||||
|
||||
input_signal = Signal(xcffib.Event)
|
||||
config_signal = Signal(xcffib.Event)
|
||||
destroy_signal = Signal(xcffib.Event)
|
||||
|
||||
def __init__(self, manager: "X11GameManager", wid: int, **kwargs):
|
||||
def __init__(self, manager: "X11GameManager", window: Window, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.manager = manager
|
||||
self.wid = wid
|
||||
self.pid = manager.get_property(wid, "_NET_WM_PID")
|
||||
self.qwindow = QWindow.fromWinId(wid)
|
||||
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
self.embedded_windows = []
|
||||
self._setup_x()
|
||||
self._setup_overlay()
|
||||
|
||||
self.input_signal.connect(self.on_input)
|
||||
self.config_signal.connect(self.on_config)
|
||||
|
||||
self.xwindow = window
|
||||
self.qwindow = QWindow.fromWinId(self.xwindow.id)
|
||||
self._update_is_focused()
|
||||
|
||||
def _setup_x(self):
|
||||
self.manager.xcomposite.RedirectWindow(
|
||||
self.wid, xcffib.composite.Redirect.Automatic
|
||||
)
|
||||
self.name_pixmap()
|
||||
self.manager.connection.core.ChangeWindowAttributesChecked(
|
||||
self.wid,
|
||||
xcffib.xproto.CW.EventMask,
|
||||
[
|
||||
xcffib.xproto.EventMask.KeyPress
|
||||
| xcffib.xproto.EventMask.StructureNotify
|
||||
],
|
||||
)
|
||||
|
||||
def _setup_overlay(self):
|
||||
self.overlay, self._overlay_disconnect = self.manager.overlay.add_instance(self)
|
||||
|
||||
def __del__(self):
|
||||
self.manager.connection.core.FreePixmap(self.pixmap_id)
|
||||
self.manager.xcomposite.UnredirectWindow(
|
||||
self.wid, xcffib.composite.Redirect.Automatic
|
||||
)
|
||||
self._overlay_disconnect()
|
||||
|
||||
def name_pixmap(self):
|
||||
if hasattr(self, "pixmap_id"):
|
||||
self.manager.connection.core.FreePixmap(self.pixmap_id)
|
||||
|
||||
self.pixmap_id = self.manager.connection.generate_id()
|
||||
self.manager.xcomposite.NameWindowPixmap(
|
||||
self.wid, self.pixmap_id, is_checked=True
|
||||
)
|
||||
|
||||
def get_position(self) -> QRect:
|
||||
if self.cached_position:
|
||||
return self.cached_position
|
||||
geom = self.xwindow.get_geometry()
|
||||
|
||||
geom = self.manager.connection.core.GetGeometry(self.wid).reply()
|
||||
translated = self.manager.connection.core.TranslateCoordinates(
|
||||
self.wid, self.manager.screen.root, 0, 0
|
||||
).reply()
|
||||
|
||||
self.cached_position = QRect(
|
||||
translated.dst_x, translated.dst_y, geom.width, geom.height
|
||||
)
|
||||
return self.cached_position
|
||||
return QRect(geom.x, geom.y, geom.width, geom.height)
|
||||
|
||||
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.xwindow.id
|
||||
|
||||
def set_taskbar_progress(self, type_, progress):
|
||||
# TODO: Implement this on Unity? I don't use Unity
|
||||
# TODO: If progress = 1 set urgency flag
|
||||
# Not supported
|
||||
pass
|
||||
|
||||
def grab_game(self):
|
||||
if (time.monotonic() - self.game_last_grab) * 1000 < self.refresh_rate:
|
||||
return self.game_last_image
|
||||
|
||||
xid = shm = None
|
||||
try:
|
||||
geom = self.manager.connection.core.GetGeometry(self.pixmap_id).reply()
|
||||
xid, shm = self.manager.get_shm(geom.width * geom.height * 4)
|
||||
size = (
|
||||
self.manager.xshm.GetImage(
|
||||
self.pixmap_id,
|
||||
0,
|
||||
0,
|
||||
geom.width,
|
||||
geom.height,
|
||||
0xFFFFFF,
|
||||
xcffib.xproto.ImageFormat.ZPixmap,
|
||||
xid,
|
||||
0,
|
||||
)
|
||||
.reply()
|
||||
.size
|
||||
)
|
||||
|
||||
out = zpixmap_shm_to_image(shm, size, geom.width, geom.height)
|
||||
|
||||
self.game_last_image = out
|
||||
self.game_last_grab = time.monotonic()
|
||||
|
||||
return out
|
||||
finally:
|
||||
if shm:
|
||||
self.manager.free_shm((xid, shm))
|
||||
|
||||
def embed_window(self, window: QWindow):
|
||||
super().embed_window(window)
|
||||
self.embedded_windows.append(window)
|
||||
|
||||
def get_overlay_area(self) -> QGraphicsItem:
|
||||
return self.overlay
|
||||
|
||||
@Slot(xcffib.Event)
|
||||
def on_config(self, evt: xcffib.xproto.ConfigureNotifyEvent):
|
||||
self.name_pixmap()
|
||||
|
||||
translated = self.manager.connection.core.TranslateCoordinates(
|
||||
self.wid, self.manager.screen.root, 0, 0
|
||||
).reply()
|
||||
size = QRect(translated.dst_x, translated.dst_y, evt.width, evt.height)
|
||||
|
||||
self.cached_position = size
|
||||
self.positionChanged.emit(size)
|
||||
|
||||
@Slot(xcffib.Event)
|
||||
def on_input(
|
||||
self, evt: Union[xcffib.xproto.KeyPressEvent, xcffib.xproto.ButtonPressEvent]
|
||||
):
|
||||
if evt.detail == 10 and evt.state & xcffib.xproto.KeyButMask.Mod1:
|
||||
self.alt1_pressed.emit()
|
||||
else:
|
||||
self.game_activity.emit()
|
||||
@@ -1,294 +1,157 @@
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import List, Dict, Tuple, Union
|
||||
from typing import List, Dict
|
||||
|
||||
import sysv_ipc
|
||||
import xcffib
|
||||
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, Signal, QObject
|
||||
from Xlib import X
|
||||
from Xlib.display import Display
|
||||
from Xlib.ext import xinput, ge
|
||||
from Xlib.protocol import event
|
||||
from Xlib.xobject.drawable import Window
|
||||
import Xlib.threaded
|
||||
|
||||
from runekit.game import GameManager
|
||||
from .instance import GameInstance, X11GameInstance
|
||||
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):
|
||||
connection: xcffib.Connection
|
||||
|
||||
_instances: Dict[int, X11GameInstance]
|
||||
_atom: Dict[bytes, int]
|
||||
_shm: List[Tuple[int, sysv_ipc.SharedMemory]]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(*kwargs)
|
||||
self._instances = {}
|
||||
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)
|
||||
self.xshm = self.connection(xcffib.shm.key)
|
||||
self.xinput = self.connection(xcffib.xinput.key)
|
||||
self._setup_composite()
|
||||
self._setup_overlay()
|
||||
|
||||
self.event_thread = QThread(self)
|
||||
|
||||
self.event_worker = X11EventWorker(self)
|
||||
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()
|
||||
self.event_thread.wait()
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
query = self.connection.core.QueryTree(wid).reply()
|
||||
for child in query.children:
|
||||
visit(child)
|
||||
except xcffib.xproto.WindowError:
|
||||
return
|
||||
|
||||
visit(self.screen.root)
|
||||
|
||||
return list(self._instances.values())
|
||||
|
||||
def get_active_instance(self) -> Union[GameInstance, None]:
|
||||
for instance in self._instances.values():
|
||||
if instance.is_focused():
|
||||
return instance
|
||||
|
||||
return None
|
||||
|
||||
def is_game(self, wid: int) -> bool:
|
||||
geom_req = self.connection.core.GetGeometry(wid)
|
||||
try:
|
||||
wm_class = self.get_property(wid, xcffib.xproto.Atom.WM_CLASS)
|
||||
except xcffib.xproto.WindowError:
|
||||
geom_req.discard_reply()
|
||||
return False
|
||||
|
||||
geom = geom_req.reply()
|
||||
if geom.width == 32 and geom.height == 32:
|
||||
# OpenGL test window
|
||||
return False
|
||||
|
||||
if not wm_class:
|
||||
return False
|
||||
|
||||
instance_name, app_name = wm_class.split("\00")
|
||||
return app_name == WM_APP_NAME
|
||||
|
||||
def get_active_window(self) -> int:
|
||||
return self.get_property(self.screen.root, "_NET_ACTIVE_WINDOW")
|
||||
|
||||
def _setup_overlay(self):
|
||||
self.overlay = DesktopWideOverlay()
|
||||
self.overlay.show()
|
||||
self.overlay.check_compatibility()
|
||||
|
||||
def _setup_composite(self):
|
||||
self.xcomposite.QueryVersion(0, 4, is_checked=True)
|
||||
|
||||
def get_property(
|
||||
self,
|
||||
wid: int,
|
||||
name: str,
|
||||
type_=xcffib.xproto.GetPropertyType.Any,
|
||||
index=0,
|
||||
max_values=1000,
|
||||
):
|
||||
reply = self.connection.core.GetProperty(
|
||||
False,
|
||||
wid,
|
||||
self.get_atom(name),
|
||||
type_,
|
||||
index,
|
||||
max_values,
|
||||
).reply()
|
||||
|
||||
if reply.type == xcffib.xproto.Atom.STRING:
|
||||
return reply.value.to_string()[:-1]
|
||||
elif reply.type in (xcffib.xproto.Atom.WINDOW, xcffib.xproto.Atom.CARDINAL):
|
||||
return struct.unpack("=I", reply.value.buf()[:4])[0]
|
||||
|
||||
return reply.value
|
||||
|
||||
def get_atom(self, atom: str) -> int:
|
||||
if isinstance(atom, int):
|
||||
return atom
|
||||
|
||||
if hasattr(xcffib.xproto.Atom, atom):
|
||||
return getattr(xcffib.xproto.Atom, atom)
|
||||
|
||||
atom = atom.encode("ascii")
|
||||
|
||||
if atom in self._atom:
|
||||
return self._atom[atom]
|
||||
|
||||
out = self.connection.core.InternAtom(False, len(atom), atom).reply().atom
|
||||
self._atom[atom] = out
|
||||
|
||||
return out
|
||||
|
||||
def get_shm(self, size: int) -> Tuple[int, sysv_ipc.SharedMemory]:
|
||||
for item in self._shm:
|
||||
if item[1].size >= size:
|
||||
self._shm.remove(item)
|
||||
return item
|
||||
|
||||
shm = sysv_ipc.SharedMemory(None, flags=sysv_ipc.IPC_CREX, size=size)
|
||||
xid = self.connection.generate_id()
|
||||
self.xshm.Attach(xid, shm.id, False, is_checked=True)
|
||||
return xid, shm
|
||||
|
||||
def free_shm(self, shm: Tuple[int, sysv_ipc.SharedMemory]):
|
||||
self._shm.append(shm)
|
||||
self.gc_shm()
|
||||
|
||||
def gc_shm(self):
|
||||
while len(self._shm) > MAX_SHM:
|
||||
xid, shm = self._shm.pop(0)
|
||||
self.xshm.Detach(xid)
|
||||
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)
|
||||
on_active_window_changed = Signal(int)
|
||||
on_alt1 = Signal(int)
|
||||
|
||||
def __init__(self, manager: "X11GameManager", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.manager = manager
|
||||
self.display = manager.display
|
||||
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
|
||||
self.handlers = {
|
||||
xcffib.xproto.PropertyNotifyEvent: self.on_property_change,
|
||||
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,
|
||||
ge.GenericEventCode: self.dispatch_ge,
|
||||
X.PropertyNotify: self.on_property_change,
|
||||
}
|
||||
self.ge_handlers = {xinput.KeyPress: self.on_key_press}
|
||||
self.active_win_id = self.manager.get_active_window()
|
||||
|
||||
self._NET_ACTIVE_WINDOW = self.display.get_atom("_NET_ACTIVE_WINDOW")
|
||||
|
||||
@Slot()
|
||||
def run(self):
|
||||
self.manager.connection.core.ChangeWindowAttributesChecked(
|
||||
self.manager.screen.root,
|
||||
xcffib.xproto.CW.EventMask,
|
||||
[
|
||||
xcffib.xproto.EventMask.PropertyChange
|
||||
| xcffib.xproto.EventMask.SubstructureNotify,
|
||||
],
|
||||
).check()
|
||||
current_thread = QThread.currentThread()
|
||||
root = self.display.screen().root
|
||||
root.change_attributes(event_mask=X.PropertyChangeMask)
|
||||
|
||||
while True:
|
||||
if current_thread.isInterruptionRequested():
|
||||
return
|
||||
|
||||
evt = self.manager.connection.poll_for_event()
|
||||
if evt is None:
|
||||
QThread.msleep(10)
|
||||
evt = self.display.next_event()
|
||||
if evt.send_event != 0:
|
||||
continue
|
||||
|
||||
for wanted_type, handler in self.handlers.items():
|
||||
if isinstance(evt, wanted_type):
|
||||
try:
|
||||
handler(evt)
|
||||
except:
|
||||
self.logger.error(
|
||||
"Error handling event %s", repr(evt), exc_info=True
|
||||
)
|
||||
try:
|
||||
handler = self.handlers.get(evt.type)
|
||||
if handler:
|
||||
handler(evt)
|
||||
except:
|
||||
self.logger.error("Error handling event %s", repr(evt), exc_info=True)
|
||||
continue
|
||||
|
||||
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
|
||||
):
|
||||
def dispatch_ge(self, evt: ge.GenericEvent):
|
||||
handler = self.ge_handlers.get(evt.evtype)
|
||||
if handler:
|
||||
handler(evt)
|
||||
|
||||
def on_key_press(self, evt):
|
||||
window = evt.data.event
|
||||
# alt1
|
||||
if evt.data.mods.effective_mods & X.Mod1Mask and evt.data.detail == 10:
|
||||
self.on_alt1.emit(window.id)
|
||||
self.logger.debug("alt1 pressed %s", repr(evt))
|
||||
|
||||
window.xinput_ungrab_keycode(evt.data.deviceid, evt.data.detail, (X.Mod1Mask,))
|
||||
self.manager._setup_grab(window)
|
||||
|
||||
def on_property_change(self, evt: event.PropertyNotify):
|
||||
if evt.atom == self._NET_ACTIVE_WINDOW:
|
||||
active_win_id = self.manager.get_active_window()
|
||||
|
||||
if self.active_win_id == active_win_id:
|
||||
return
|
||||
|
||||
self.active_win_id = active_win_id
|
||||
self.logger.debug(
|
||||
"Active window changed to %d - %s", active_win_id, repr(evt)
|
||||
)
|
||||
self.on_active_window_changed.emit(active_win_id)
|
||||
|
||||
for id_, instance in self.manager._instances.items():
|
||||
active = active_win_id == id_
|
||||
|
||||
if active != instance._is_focused:
|
||||
instance._is_focused = active
|
||||
instance.focusChanged.emit(active)
|
||||
class X11GameManager(GameManager):
|
||||
display: Display
|
||||
|
||||
def on_input_event(
|
||||
self, evt: Union[xcffib.xproto.KeyPressEvent, xcffib.xproto.ButtonPressEvent]
|
||||
):
|
||||
try:
|
||||
self.manager._instances[evt.event].input_signal.emit(evt)
|
||||
except KeyError:
|
||||
self.logger.debug("Got input event for %d but is not registered", evt.event)
|
||||
_instance: Dict[int, X11GameInstance]
|
||||
|
||||
def on_configure_event(self, evt: xcffib.xproto.ConfigureNotifyEvent):
|
||||
try:
|
||||
self.manager._instances[evt.window].config_signal.emit(evt)
|
||||
except KeyError:
|
||||
pass
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(*kwargs)
|
||||
self.display = Display()
|
||||
self._NET_ACTIVE_WINDOW = self.display.get_atom("_NET_ACTIVE_WINDOW")
|
||||
self._instance = {}
|
||||
|
||||
def on_create(self, evt: xcffib.xproto.CreateNotifyEvent):
|
||||
if evt.window in self.manager._instances:
|
||||
return
|
||||
self.event_thread = QThread()
|
||||
|
||||
# At this point we aren't sure if it's actually the game...
|
||||
if not self.manager.is_game(evt.window):
|
||||
return
|
||||
self.event_worker = X11EventWorker(self)
|
||||
self.event_worker.moveToThread(self.event_thread)
|
||||
self.event_thread.started.connect(self.event_worker.run)
|
||||
self.event_worker.on_active_window_changed.connect(
|
||||
self.on_active_window_changed
|
||||
)
|
||||
self.event_worker.on_alt1.connect(self.on_alt1)
|
||||
|
||||
self.create_signal.emit(evt)
|
||||
self.event_thread.start()
|
||||
|
||||
def on_destroy(self, evt: xcffib.xproto.DestroyNotifyEvent):
|
||||
if evt.window in self.manager._instances:
|
||||
self.destroy_signal.emit(evt.window)
|
||||
def get_instances(self) -> List[GameInstance]:
|
||||
def visit(window):
|
||||
try:
|
||||
wm_class = window.get_wm_class()
|
||||
except:
|
||||
return
|
||||
|
||||
if wm_class and wm_class[0] == "RuneScape":
|
||||
if window.id not in self._instance:
|
||||
instance = X11GameInstance(self, window, parent=self)
|
||||
self._setup_grab(window)
|
||||
self._instance[window.id] = instance
|
||||
|
||||
for child in window.query_tree().children:
|
||||
visit(child)
|
||||
|
||||
visit(self.display.screen().root)
|
||||
|
||||
return list(self._instance.values())
|
||||
|
||||
def _setup_grab(self, window: Window):
|
||||
# alt1
|
||||
window.xinput_grab_keycode(
|
||||
xinput.AllDevices,
|
||||
X.CurrentTime,
|
||||
10,
|
||||
xinput.GrabModeAsync,
|
||||
xinput.GrabModeAsync,
|
||||
True,
|
||||
xinput.KeyPressMask,
|
||||
(X.Mod1Mask,),
|
||||
)
|
||||
|
||||
def get_active_window(self) -> int:
|
||||
resp = self.display.screen().root.get_full_property(
|
||||
self._NET_ACTIVE_WINDOW, X.AnyPropertyType
|
||||
)
|
||||
return resp.value[0]
|
||||
|
||||
@Slot(int)
|
||||
def on_active_window_changed(self, winid):
|
||||
for id_, instance in self._instance.items():
|
||||
active = winid == id_
|
||||
|
||||
if active != instance.is_focused:
|
||||
instance.focusChanged.emit(active)
|
||||
|
||||
instance.is_focused = active
|
||||
|
||||
@Slot(int)
|
||||
def on_alt1(self, winid):
|
||||
if winid in self._instance:
|
||||
self._instance[winid].alt1_pressed.emit()
|
||||
@@ -1,13 +0,0 @@
|
||||
import sysv_ipc
|
||||
import numpy as np
|
||||
from PIL import ImageOps, Image
|
||||
|
||||
|
||||
def zpixmap_shm_to_image(
|
||||
shm: sysv_ipc.SharedMemory, size: int, width: int, height: int
|
||||
) -> np.ndarray:
|
||||
out = np.frombuffer(shm.read(size), "<B").copy()
|
||||
out.shape = (height, width, 4)
|
||||
out[:, :, 3] = 0xFF # Convert BGRX to BGRA
|
||||
|
||||
return out
|
||||
@@ -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
|
||||
@@ -1,116 +1,29 @@
|
||||
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 PySide2.QtWidgets import QWidget
|
||||
|
||||
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.host.settings import SettingsDialog
|
||||
from runekit.alt1.utils import fetch_manifest
|
||||
from runekit.app import App
|
||||
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:
|
||||
class Host(QWidget):
|
||||
"""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)
|
||||
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)
|
||||
app.get_window(parent=self).show()
|
||||
@@ -1,221 +0,0 @@
|
||||
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.QtWidgets import (
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QMainWindow,
|
||||
QWidget,
|
||||
QFormLayout,
|
||||
QLabel,
|
||||
QComboBox,
|
||||
QCheckBox,
|
||||
QTableWidget,
|
||||
QHBoxLayout,
|
||||
QTreeView,
|
||||
QSpinBox,
|
||||
QPushButton,
|
||||
QAbstractItemView,
|
||||
QInputDialog,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
from .appstore_model import AppStoreModel
|
||||
from ..ui import TooltipNotifier, TrayIconNotifier, AutoNotifier
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .host import Host
|
||||
|
||||
|
||||
class SettingsDialog(QMainWindow):
|
||||
def __init__(self, host: "Host", **kwargs):
|
||||
super().__init__(flags=Qt.Dialog, **kwargs)
|
||||
self.host = host
|
||||
self._layout()
|
||||
self.setWindowTitle("RuneKit Settings")
|
||||
|
||||
def _layout(self):
|
||||
tab = QTabWidget(self)
|
||||
tab.addTab(ApplicationPage(self.host, parent=self), "Applications")
|
||||
tab.addTab(InterfacePage(self.host, parent=self), "Interface")
|
||||
|
||||
self.setCentralWidget(tab)
|
||||
self.setContentsMargins(11, 11, 11, 11)
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return QSize(800, 500)
|
||||
|
||||
|
||||
class ApplicationPage(QWidget):
|
||||
host: "Host"
|
||||
|
||||
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???
|
||||
|
||||
|
||||
class InterfacePage(QWidget):
|
||||
host: "Host"
|
||||
|
||||
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.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)
|
||||
layout.addRow(None, border_field)
|
||||
|
||||
@Slot(int)
|
||||
def preview_tooltip(self, index: int):
|
||||
type_ = list(AutoNotifier.availableMethods().items())[index][0]
|
||||
if type_ == AutoNotifier.METHOD_NOTIFICATION:
|
||||
notifier = TrayIconNotifier()
|
||||
elif type_ == AutoNotifier.METHOD_TOOLTIP:
|
||||
notifier = TooltipNotifier()
|
||||
else:
|
||||
return
|
||||
|
||||
if hasattr(self, "_last_notifier"):
|
||||
self._last_notifier.notify("")
|
||||
|
||||
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)
|
||||
@@ -1 +0,0 @@
|
||||
from .algo import is_color_percent_gte
|
||||
@@ -0,0 +1,55 @@
|
||||
from ._utils cimport image_to_ptr, unblend, ImageRGBA, RGB
|
||||
|
||||
cdef struct Rectangle:
|
||||
int x
|
||||
int y
|
||||
int width
|
||||
int height
|
||||
|
||||
def get_chat_color(image, tuple box_, list colors):
|
||||
cdef Rectangle box = Rectangle(box_[0], box_[1], box_[2], box_[3])
|
||||
|
||||
cdef float best_score = -1.0
|
||||
cdef RGB best = RGB(0, 0, 0)
|
||||
cdef ImageRGBA **data = image_to_ptr(image)
|
||||
|
||||
cdef double score
|
||||
cdef ImageRGBA i1, i2
|
||||
cdef (unsigned char, unsigned char) pixel1, pixel2
|
||||
cdef int x, y
|
||||
cdef RGB color
|
||||
|
||||
for color in colors:
|
||||
score = 0.0
|
||||
for y in range(box.y, box.y + box.height):
|
||||
for x in range(box.x, box.x + box.width):
|
||||
if x < 0 or x + 1 >= box.height:
|
||||
continue
|
||||
if y < 0 or y + 1 >= box.width:
|
||||
continue
|
||||
i1 = data[y][x]
|
||||
i2 = data[y + 1][x + 1]
|
||||
pixel1 = unblend(i1.r, i1.g, i1.b, color.r, color.g, color.b)
|
||||
|
||||
pixel2 = unblend(i2.r, i2.g, i2.b, color.r, color.g, color.b)
|
||||
score += (pixel1[0] / 255 * pixel1[1] / 255) * (pixel2[0] / 255 * (255.0 - pixel2[1]) / 255)
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = color
|
||||
|
||||
return (best.r, best.g, best.b)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
cdef struct CharInfo:
|
||||
int width
|
||||
int height
|
||||
Py_UNICODE chr
|
||||
int bonus
|
||||
bint secondary
|
||||
|
||||
unsigned char *pixels
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
from cpython.mem cimport PyMem_Malloc, PyMem_Free
|
||||
from ._font cimport CharInfo
|
||||
from ._utils cimport image_to_ptr, ImageRGBA
|
||||
|
||||
cdef class FontDefinition:
|
||||
cdef CharInfo *chars
|
||||
cdef size_t chars_len
|
||||
cdef readonly int width
|
||||
cdef readonly int height
|
||||
cdef readonly int space_width
|
||||
cdef readonly bint shadow
|
||||
cdef readonly int base_y
|
||||
|
||||
cdef readonly int min_rating
|
||||
cdef readonly int max_spaces
|
||||
|
||||
def __dealloc__(self):
|
||||
for i in range(self.chars_len):
|
||||
PyMem_Free(self.chars[i].pixels)
|
||||
|
||||
PyMem_Free(self.chars)
|
||||
|
||||
cpdef FontDefinition generate_font(image, str chars, str seconds, bonuses, int base_y, int space_width, int threshold, bint shadow):
|
||||
cdef int chars_len = len(chars)
|
||||
cdef FontDefinition out = FontDefinition()
|
||||
cdef CharInfo *cinfo = <CharInfo*> PyMem_Malloc(sizeof(CharInfo) * chars_len)
|
||||
cdef ImageRGBA **raw = image_to_ptr(image)
|
||||
|
||||
cdef int width = image.width
|
||||
cdef int height = image.height
|
||||
cdef int min_y = image.height - 1
|
||||
cdef int max_y = 0
|
||||
cdef int ds = -1
|
||||
cdef int i, dx, cur_char, letter_width, x, y
|
||||
cdef Py_UNICODE letter
|
||||
cdef CharInfo *chr
|
||||
|
||||
threshold *= 255
|
||||
|
||||
for dx in range(width):
|
||||
if ds == -1:
|
||||
i = height - 1
|
||||
if raw[i][dx].r == 255 and raw[i][dx].a == 255:
|
||||
ds = dx
|
||||
|
||||
continue
|
||||
|
||||
letter = chars[cur_char]
|
||||
letter_width = ds - dx
|
||||
cinfo[cur_char] = CharInfo(
|
||||
width=letter_width,
|
||||
chr=letter,
|
||||
bonus=0,
|
||||
secondary=letter in seconds,
|
||||
)
|
||||
if bonuses and letter in bonuses:
|
||||
cinfo[cur_char].bonus = bonuses[letter]
|
||||
|
||||
cur_char += 1
|
||||
out.width = max(out.width, letter_width)
|
||||
|
||||
for y in range(height - 1):
|
||||
for x in range(letter_width):
|
||||
if raw[y][x + ds].r >= threshold:
|
||||
min_y = min(min_y, y)
|
||||
max_y = max(max_y, y)
|
||||
|
||||
ds = -1
|
||||
|
||||
out.height = max_y + 1 - min_y
|
||||
out.base_y = base_y - min_y
|
||||
|
||||
for a in range(chars_len):
|
||||
chr = &cinfo[a]
|
||||
# TODO
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,16 @@
|
||||
cdef struct ImageRGBA:
|
||||
unsigned char r
|
||||
unsigned char g
|
||||
unsigned char b
|
||||
unsigned char a
|
||||
|
||||
cdef struct RGB:
|
||||
unsigned char r
|
||||
unsigned char g
|
||||
unsigned char b
|
||||
|
||||
cpdef (unsigned char, unsigned char) unblend(int r, int g, int b, int R, int G, int B) nogil
|
||||
cdef ImageRGBA** image_to_ptr(image)
|
||||
cpdef int can_blend(int rm, int gm, int bm, int r1, int g1, int b1, int p) nogil
|
||||
# cdef (double, double, double) decompose2col(unsigned char rp, unsigned char gp, unsigned char bp, unsigned char r1, unsigned char g1, unsigned char b1, unsigned char r2, unsigned char g2, unsigned char b2) nogil
|
||||
# cdef (double, double, double) decompose3col(unsigned char rp, unsigned char gp, unsigned char bp, unsigned char r1, unsigned char g1, unsigned char b1, unsigned char r2, unsigned char g2, unsigned char b2, double r3, double g3, double b3) nogil
|
||||
@@ -0,0 +1,116 @@
|
||||
from PIL.Image import new as image_new
|
||||
from libc.math cimport sqrt, fabs
|
||||
from ._utils cimport ImageRGBA
|
||||
|
||||
cpdef (unsigned char, unsigned char) unblend(int r, int g, int b, int R, int G, int B) nogil:
|
||||
cdef double m = sqrt(r * r + g * g + b * b)
|
||||
cdef double n = sqrt(R * R + G * G + B * B)
|
||||
|
||||
cdef double x = (r * R + g * G + b * B) / n
|
||||
cdef double y = sqrt(max(0.0, m * m - x * x))
|
||||
|
||||
cdef int r1 = int(max(0.0, (63.75 - y) * 4))
|
||||
cdef int r2 = int(x / n * 255)
|
||||
|
||||
if r2 > 255:
|
||||
r1 = max(0, r1 - r2 + 255)
|
||||
r2 = 255
|
||||
|
||||
return r1, r2
|
||||
|
||||
cdef ImageRGBA** image_to_ptr(image):
|
||||
ptr = dict(image.im.unsafe_ptrs)['image32']
|
||||
cdef size_t ptr_int = <size_t> ptr
|
||||
return <ImageRGBA**> ptr_int
|
||||
|
||||
def rgb(unsigned char r, unsigned char g, unsigned char b):
|
||||
return RGB(r, g,b)
|
||||
|
||||
cpdef int can_blend(int rm, int gm, int bm, int r1, int g1, int b1, int p) nogil:
|
||||
"""
|
||||
Determine whether color [rgb]m can be a result of a blend with color [rgb]1 that is p (0-1) of the mix
|
||||
:param rm: resulting color
|
||||
:param r1: first color of the mix (the other color is unknown)
|
||||
:param p: the portion of the [rgb]1 in the mix (0-1)
|
||||
:return: the number that the second color has to lie outside of the possible color ranges
|
||||
"""
|
||||
cdef int m = min(50, p/(1-p))
|
||||
cdef int r = rm + (rm - r1) * m
|
||||
cdef int g = gm + (gm - g1) * m
|
||||
cdef int b = bm + (bm - b1) * m
|
||||
return max(-r, -g, -b, r - 255, g - 255, b - 255)
|
||||
|
||||
def unblend_known_background(img, bg_img, bint shadow, unsigned char r, unsigned char g, unsigned char b):
|
||||
if bg_img:
|
||||
assert img.width == bg_img.width and img.height == bg_img.height
|
||||
|
||||
out_img = image_new('RGBA', (img.width, img.height), None)
|
||||
|
||||
cdef ImageRGBA **outptr = image_to_ptr(out_img)
|
||||
cdef ImageRGBA **imgptr = image_to_ptr(img)
|
||||
cdef ImageRGBA **bgptr = image_to_ptr(bg_img)
|
||||
cdef int width = img.width
|
||||
cdef int height = img.height
|
||||
|
||||
cdef int x, y
|
||||
cdef (double, double, double) col
|
||||
cdef double m
|
||||
cdef unsigned char red
|
||||
|
||||
with nogil:
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
pixel = imgptr[y][x]
|
||||
bg_pixel = bgptr[y][x]
|
||||
|
||||
col = decompose2col(pixel.r, pixel.g, pixel.b, r, g, b, bg_pixel.r, bg_pixel.g, bg_pixel.b)
|
||||
|
||||
if shadow:
|
||||
m = 1 - col[1] - fabs(col[2])
|
||||
red = <unsigned char> (m * 255)
|
||||
outptr[y][x].r = red
|
||||
outptr[y][x].g = <unsigned char> (col[0] / m * 255.0)
|
||||
outptr[y][x].b = red
|
||||
else:
|
||||
red = <unsigned char> (col[0] * 255)
|
||||
outptr[y][x].r = red
|
||||
outptr[y][x].g = red
|
||||
outptr[y][x].b = red
|
||||
|
||||
outptr[y][x].a = 255
|
||||
|
||||
return out_img
|
||||
|
||||
cdef inline (double, double, double) decompose2col(double rp, double gp, double bp, double r1, double g1, double b1, double r2, double g2, double b2) nogil:
|
||||
cdef double r3 = g1 * b2 - g2 * b1
|
||||
cdef double g3 = b1 * r2 - b2 * r1
|
||||
cdef double b3 = r1 * g2 - r2 * g1
|
||||
|
||||
cdef double norm = 255.0 / sqrt(r3 * r3 + g3 * g3 + b3 * b3)
|
||||
r3 *= norm
|
||||
g3 *= norm
|
||||
b3 *= norm
|
||||
|
||||
return decompose3col(rp, gp, bp, r1, g1, b1, r2, g2, b2, r3, g3, b3)
|
||||
|
||||
cdef inline (double, double, double) decompose3col(double rp, double gp, double bp, double r1, double g1, double b1, double r2, double g2, double b2, double r3, double g3, double b3) nogil:
|
||||
cdef double A = g2 * b3 - b2 * g3
|
||||
cdef double B = g3 * b1 - b3 * g1
|
||||
cdef double C = g1 * b2 - b1 * g2
|
||||
|
||||
cdef double D = b2 * r3 - r2 * b3
|
||||
cdef double E = b3 * r1 - r3 * b1
|
||||
cdef double F = b1 * r2 - r1 * b2
|
||||
|
||||
cdef double G = r2 * g3 - g2 * r3
|
||||
cdef double H = r3 * g1 - g3 * r1
|
||||
cdef double I = r1 * g2 - g1 * r2
|
||||
|
||||
cdef double det = r1 * A + g1 * D + b1 * G
|
||||
|
||||
cdef double x = (A * rp + D * gp + G * bp) / det
|
||||
cdef double y = (B * rp + E * gp + H * bp) / det
|
||||
cdef double z = (C * rp + F * gp + I * bp) / det
|
||||
|
||||
return x, y, z
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def is_color_percent_gte(image: np.ndarray, color: List, percent: float) -> bool:
|
||||
"""Check that the image has at least percent% of the given color"""
|
||||
black_pixels = np.count_nonzero(np.all(image[:, :, : len(color)] == color, axis=-1))
|
||||
total_pixels = image.shape[0] * image.shape[1]
|
||||
color_percent = black_pixels / total_pixels
|
||||
return color_percent >= percent
|
||||
@@ -0,0 +1,291 @@
|
||||
from typing import List, Tuple, Optional, TypedDict, NamedTuple
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from runekit.alt1.schema import FontMeta
|
||||
from ._chat import get_chat_color
|
||||
from ._utils import rgb, can_blend
|
||||
|
||||
Color = Tuple[int, int, int]
|
||||
|
||||
|
||||
class Rectangle(NamedTuple):
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
class OCRLine(NamedTuple):
|
||||
text: str
|
||||
area: Rectangle
|
||||
|
||||
|
||||
class ChatboxLineFragment(TypedDict):
|
||||
color: int
|
||||
text: str
|
||||
index: int
|
||||
|
||||
|
||||
class ReadCharInfo(NamedTuple):
|
||||
chr: str
|
||||
base_char: CharInfo
|
||||
x: int
|
||||
y: int
|
||||
score: float
|
||||
size_score: float
|
||||
|
||||
|
||||
def read_chatbox_line(
|
||||
image: Image,
|
||||
x: int,
|
||||
y: int,
|
||||
colors: List[Color],
|
||||
font: FontDefinition,
|
||||
backwards=False,
|
||||
allow_gap=False,
|
||||
budget=15,
|
||||
allow_multi_col=True,
|
||||
):
|
||||
colors = [rgb(*x) for x in colors]
|
||||
|
||||
fragment_count = 0
|
||||
max_fragment_len = 0
|
||||
scan_x = x
|
||||
text = ""
|
||||
fragments: List[ChatboxLineFragment] = []
|
||||
|
||||
total_space_size = 0
|
||||
while True:
|
||||
chat_color = get_chat_color(image, (scan_x, y - 8, 6, 8), colors)
|
||||
ocr_line = read_chat_line(
|
||||
image,
|
||||
chat_color,
|
||||
font,
|
||||
scan_x,
|
||||
y,
|
||||
backward=backwards and fragment_count == 0,
|
||||
forward=True,
|
||||
scan_mode=True,
|
||||
)
|
||||
|
||||
if not ocr_line or not ocr_line.text or not ocr_line.area[4]:
|
||||
if not allow_multi_col and fragment_count != 0:
|
||||
break
|
||||
if total_space_size >= font["width"] and (
|
||||
not allow_gap
|
||||
or total_space_size >= 20
|
||||
or scan_x - x >= 100
|
||||
or budget < 0
|
||||
):
|
||||
break
|
||||
|
||||
space_size = font["space_width"]
|
||||
if total_space_size != 0 or fragment_count == 0:
|
||||
space_size = 1
|
||||
|
||||
scan_x += space_size
|
||||
total_space_size += space_size
|
||||
budget -= 1
|
||||
continue
|
||||
|
||||
if total_space_size >= 2 and ocr_line.text:
|
||||
text += " "
|
||||
if len(fragments) > 0:
|
||||
fragments[-1]["text"] += " "
|
||||
|
||||
coded_color = chat_color[0] * 256 * 256 + chat_color[1] * 256 + chat_color[2]
|
||||
fragments.append(
|
||||
{
|
||||
"color": coded_color,
|
||||
"text": ocr_line.text,
|
||||
"index": len(text),
|
||||
}
|
||||
)
|
||||
fragment_count += 1
|
||||
text += ocr_line.text
|
||||
max_fragment_len = max(max_fragment_len, len(ocr_line.text))
|
||||
scan_x = max(
|
||||
scan_x + font["space_width"], ocr_line.area.x + ocr_line.area.width
|
||||
)
|
||||
total_space_size = 0
|
||||
|
||||
if len(fragments) == 0 or max_fragment_len <= 2:
|
||||
return None
|
||||
|
||||
return {
|
||||
"fragments": fragments,
|
||||
"text": text,
|
||||
}
|
||||
|
||||
|
||||
def read_chat_line(
|
||||
image: Image,
|
||||
ref_color: Color,
|
||||
font: FontDefinition,
|
||||
x: int,
|
||||
y: int,
|
||||
backward=False,
|
||||
forward=True,
|
||||
scan_mode=False,
|
||||
) -> Optional[OCRLine]:
|
||||
text = ""
|
||||
w_fw = x
|
||||
w_bw = x
|
||||
|
||||
if forward:
|
||||
shift_x = 0
|
||||
last_not_found = False
|
||||
cur_text = ""
|
||||
while True:
|
||||
flag2 = scan_mode and not text # FIXME: What's my name?
|
||||
char = read_chat_char(
|
||||
image,
|
||||
ref_color,
|
||||
font,
|
||||
x + shift_x,
|
||||
y,
|
||||
backward=False,
|
||||
allow_secondary=not flag2,
|
||||
)
|
||||
if not char:
|
||||
if flag2:
|
||||
backward = False
|
||||
break
|
||||
if last_not_found:
|
||||
break
|
||||
|
||||
cur_text += " "
|
||||
shift_x += font["space_width"]
|
||||
last_not_found = True
|
||||
else:
|
||||
last_not_found = False
|
||||
shift_x += char.base_char["width"]
|
||||
|
||||
if char.base_char.secondary:
|
||||
cur_text += char.chr
|
||||
continue
|
||||
|
||||
text = text + cur_text + char.chr
|
||||
cur_text = ""
|
||||
w_fw = x + shift_x
|
||||
|
||||
if backward:
|
||||
shift_x = 0
|
||||
last_not_found = False
|
||||
cur_text = ""
|
||||
while True:
|
||||
char = read_chat_char(image, ref_color, font, x + shift_x, y, backward=True)
|
||||
|
||||
if not char:
|
||||
if last_not_found:
|
||||
break
|
||||
cur_text = " " + cur_text
|
||||
shift_x -= font["space_width"]
|
||||
last_not_found = True
|
||||
continue
|
||||
|
||||
last_not_found = False
|
||||
shift_x -= char.base_char.width
|
||||
if char.base_char.secondary:
|
||||
cur_text = char.chr + cur_text
|
||||
continue
|
||||
text = char.chr + cur_text + text
|
||||
cur_text = ""
|
||||
w_bw = x + shift_x
|
||||
|
||||
return OCRLine(text=text, area=Rectangle(w_bw, y - 9, w_fw - w_bw, 10))
|
||||
|
||||
|
||||
class _chatScore(NamedTuple):
|
||||
score: int
|
||||
size_score: int
|
||||
chr: CharInfo
|
||||
|
||||
|
||||
def read_chat_char(
|
||||
image: Image,
|
||||
ref_color: Color,
|
||||
font: FontDefinition,
|
||||
x: int,
|
||||
y: int,
|
||||
backward=False,
|
||||
allow_secondary=True,
|
||||
) -> Optional[ReadCharInfo]:
|
||||
y -= font["base_y"]
|
||||
shadow = font["shadow"]
|
||||
|
||||
# Boundary check
|
||||
if y < 0 or y + font["height"] >= image.height:
|
||||
return None
|
||||
if not backward:
|
||||
if x < 0 or x + font["width"] > image.width:
|
||||
return None
|
||||
else:
|
||||
if x - font["width"] < 0 or x > image.width:
|
||||
return None
|
||||
|
||||
scores: List[_chatScore] = []
|
||||
for chr in font["chars"]:
|
||||
if chr.secondary and not allow_secondary:
|
||||
continue
|
||||
if backward:
|
||||
chrx = x - chr.width
|
||||
else:
|
||||
chrx = x
|
||||
|
||||
a = 0
|
||||
score = 0
|
||||
while a < len(chr.pixels):
|
||||
ix = chrx + chr.pixels[a]
|
||||
iy = y + chr.pixels[a + 1]
|
||||
pixel = image.getpixel((ix, iy))
|
||||
if not shadow:
|
||||
penalty = can_blend(
|
||||
pixel[0],
|
||||
pixel[1],
|
||||
pixel[2],
|
||||
ref_color[0],
|
||||
ref_color[1],
|
||||
ref_color[2],
|
||||
chr.pixels[a + 2] / 255,
|
||||
)
|
||||
a += 3
|
||||
else:
|
||||
lum = chr.pixels[a + 3] / 255
|
||||
penalty = can_blend(
|
||||
pixel[0],
|
||||
pixel[1],
|
||||
pixel[2],
|
||||
ref_color[0] * lum,
|
||||
ref_color[1] * lum,
|
||||
ref_color[2] * lum,
|
||||
chr.pixels[a + 2] / 255,
|
||||
)
|
||||
a += 4
|
||||
|
||||
score += max(0, penalty)
|
||||
|
||||
size_score = score - chr.bonus
|
||||
scores.append(_chatScore(score=score, size_score=size_score, chr=chr))
|
||||
|
||||
scores.sort(key=lambda item: item.size_score)
|
||||
|
||||
if not scores:
|
||||
return None
|
||||
|
||||
winner = scores[0]
|
||||
if winner.score > 400:
|
||||
return None
|
||||
|
||||
shift_x = 0
|
||||
shift_y = font["base_y"]
|
||||
|
||||
return ReadCharInfo(
|
||||
chr=winner.chr.chr,
|
||||
base_char=winner.chr,
|
||||
x=x + shift_x,
|
||||
y=y + shift_y,
|
||||
score=winner.score,
|
||||
size_score=winner.size_score,
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import TypedDict, NamedTuple, List, Optional
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from runekit.alt1.schema import FontMeta
|
||||
from ._utils import unblend_known_background
|
||||
|
||||
FONT_BASE = Path(__file__).parent / "fonts"
|
||||
|
||||
|
||||
class CharInfo(NamedTuple):
|
||||
width: int
|
||||
chr: str
|
||||
bonus: int
|
||||
secondary: bool
|
||||
pixels: List[int]
|
||||
|
||||
|
||||
class FontDefinition(TypedDict):
|
||||
chars: List[CharInfo]
|
||||
width: int
|
||||
height: int
|
||||
space_width: int
|
||||
shadow: bool
|
||||
base_y: int
|
||||
min_rating: Optional[int]
|
||||
max_spaces: Optional[int]
|
||||
|
||||
|
||||
def load_font(name: str) -> FontDefinition:
|
||||
meta_file = FONT_BASE / (name + ".fontmeta.json")
|
||||
with meta_file.open() as fp:
|
||||
meta: FontMeta = json.load(fp)
|
||||
|
||||
return load_font_file(FONT_BASE / (name + ".data.png"), meta)
|
||||
|
||||
|
||||
def load_font_file(image: Path, meta: FontMeta) -> FontDefinition:
|
||||
img = Image.open(image)
|
||||
return load_font_image(img, meta)
|
||||
|
||||
|
||||
def load_font_image(image: Image, meta: FontMeta) -> FontDefinition:
|
||||
if meta["unblendmode"] == "removebg":
|
||||
bg_height = int((image.height - 1) / 2)
|
||||
background = image.crop((0, bg_height + 1, image.width, image.height))
|
||||
font_img = unblend_known_background(
|
||||
image.crop((0, 0, image.width, bg_height)),
|
||||
background,
|
||||
meta["shadow"],
|
||||
meta["color"][0],
|
||||
meta["color"][1],
|
||||
meta["color"][2],
|
||||
)
|
||||
font_img.show()
|
||||
elif meta["unblendmode"] == "raw":
|
||||
raise NotImplementedError
|
||||
elif meta["unblendmode"] == "blackbg":
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_font("chat_8px")
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"basey": 11,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'",
|
||||
"spriteid": 17814
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 17,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'|"
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "blackbg",
|
||||
"shadow": false,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": true,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz:()0123456789%,.'?",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 13,
|
||||
"spacewidth": 5,
|
||||
"treshold": 0.6,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'|*"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 12,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 13,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"basey": 15,
|
||||
"spacewidth": 6,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'",
|
||||
"//":"Seems to have a 2px letter spacing and 4px spacewidth, the letter spacing is included in the font definition after the characters"
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 10,
|
||||
"spacewidth": 3,
|
||||
"treshold": 0.6,
|
||||
"color": [ 127, 169, 255 ],
|
||||
"unblendmode": "removebg",
|
||||
"shadow": true,
|
||||
"chars": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%/+?!@#$^~&*()_-=[]{}:;\"'<>\\.,|",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 8,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.9,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": false,
|
||||
"chars": "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
"seconds": ",.-:;\"'"
|
||||
}
|
||||
|
After Width: | Height: | Size: 530 B |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"basey": 7,
|
||||
"spacewidth": 4,
|
||||
"treshold": 0.9,
|
||||
"color": [ 255, 255, 255 ],
|
||||
"unblendmode": "raw",
|
||||
"shadow": true,
|
||||
"chars": "0123456789m()",
|
||||
"seconds": ""
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from runekit.game.instance import ImageType
|
||||
|
||||
|
||||
def np_crop(image: np.ndarray, x: int, y: int, w: int, h: int) -> np.ndarray:
|
||||
shape = image.shape
|
||||
img_width = shape[1]
|
||||
img_height = shape[0]
|
||||
if w is None:
|
||||
w = img_width
|
||||
if h is None:
|
||||
h = img_height
|
||||
|
||||
x_pad_left = max(0, -x)
|
||||
y_pad_left = max(0, -y)
|
||||
|
||||
x1 = max(0, x)
|
||||
y1 = max(0, y)
|
||||
x2 = min(x1 + w - x_pad_left, img_width - x_pad_left)
|
||||
y2 = min(y1 + h - y_pad_left, img_height - y_pad_left)
|
||||
image = image[y1:y2, x1:x2]
|
||||
|
||||
x_pad = (x_pad_left, w - (x2 - x1) - x_pad_left)
|
||||
y_pad = (y_pad_left, h - (y2 - y1) - y_pad_left)
|
||||
if x_pad != (0, 0) or y_pad != (0, 0):
|
||||
image = np.pad(image, (y_pad, x_pad, (0, 0)))
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def np_save_image(image: np.ndarray, out: str):
|
||||
from PIL import Image
|
||||
|
||||
size = (image.shape[1], image.shape[0])
|
||||
if len(image.shape) == 2:
|
||||
# Grayscale
|
||||
Image.frombuffer("L", size, image.tobytes(), "raw", "L", 0, 1).save(out)
|
||||
elif image.shape[2] == 3:
|
||||
# RGB
|
||||
Image.frombuffer(
|
||||
"RGB", size, image[:, :, ::-1].tobytes(), "raw", "RGB", 0, 1
|
||||
).save(out)
|
||||
else:
|
||||
# RGBA
|
||||
Image.frombuffer(
|
||||
"RGBA", size, image[:, :, [2, 1, 0, 3]].tobytes(), "raw", "RGBA", 0, 1
|
||||
).save(out)
|
||||
|
||||
|
||||
def ensure_np_image(image: "ImageType") -> np.ndarray:
|
||||
if isinstance(image, np.ndarray):
|
||||
return image
|
||||
|
||||
if image.mode == "RGB":
|
||||
image = image.convert("RGBA")
|
||||
|
||||
out = np.array(image)
|
||||
out = out[:, :, [2, 1, 0, 3]] # B G R A
|
||||
return out
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -1,11 +1,13 @@
|
||||
import abc
|
||||
from typing import Union, Dict
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from PySide2.QtCore import QObject, QSettings
|
||||
from PySide2.QtGui import QIcon
|
||||
from PySide2.QtWidgets import QSystemTrayIcon
|
||||
|
||||
from .tooltip import TooltipManager
|
||||
from .tray import tray_icon
|
||||
|
||||
TRAY_ICON = Path(__file__).parent / "trayicon.png"
|
||||
|
||||
|
||||
class Notifier(abc.ABC):
|
||||
@@ -14,11 +16,6 @@ class Notifier(abc.ABC):
|
||||
...
|
||||
|
||||
|
||||
class StubNotifier(Notifier):
|
||||
def notify(self, text):
|
||||
pass
|
||||
|
||||
|
||||
class TooltipNotifier(Notifier):
|
||||
def __init__(self):
|
||||
self.manager = TooltipManager()
|
||||
@@ -28,46 +25,26 @@ class TooltipNotifier(Notifier):
|
||||
|
||||
|
||||
class TrayIconNotifier(Notifier):
|
||||
def __init__(self):
|
||||
self.icon = QSystemTrayIcon(QIcon(str(TRAY_ICON)))
|
||||
self.icon.show()
|
||||
|
||||
def notify(self, text):
|
||||
if text:
|
||||
tray_icon().showMessage("RuneKit", text)
|
||||
self.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
|
||||
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 941 B |
|
Before Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 182 B |
|
Before Width: | Height: | Size: 323 B |