Compare commits
No files matched your search
@@ -0,0 +1,53 @@
|
||||
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 }}
|
||||
@@ -0,0 +1,47 @@
|
||||
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 }}
|
||||
@@ -0,0 +1,21 @@
|
||||
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 }}
|
||||
@@ -4,3 +4,7 @@ runekit/image/format.c
|
||||
runekit/image/format.*.*
|
||||
setup.py
|
||||
*.egg-info
|
||||
__pycache__
|
||||
runekit/_resources.py
|
||||
*.AppImage
|
||||
*.pyo
|
||||
@@ -14,6 +14,22 @@ 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.
|
||||
@@ -25,9 +41,17 @@ Packages related to Alt1 app.
|
||||
|
||||
## UI
|
||||
|
||||
- host: Main window of RuneKit
|
||||
- host: RuneKit Qt root
|
||||
- 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
|
||||
@@ -0,0 +1,47 @@
|
||||
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,48 +4,39 @@
|
||||
|
||||
Alt1-compatible toolbox for RuneScape 3, for Linux and macOS.
|
||||
|
||||
**Alpha quality software**
|
||||
**See also [Alt1 Electron](https://github.com/skillbert/alt1-electron) the experimental official Alt1 replacement from the Alt1 authors.**
|
||||
|
||||
## What works
|
||||
* [Compatibility](https://github.com/whs/runekit/wiki/App-Compatibility)
|
||||
* [Troubleshooting](https://github.com/whs/runekit/wiki/Troubleshooting)
|
||||
|
||||
**Overlay are not supported yet**
|
||||
## Installing
|
||||
|
||||
App | Linux | macOS
|
||||
-------------------------------|-------|----------
|
||||
**Clue Solver** ||
|
||||
- Text/Emote | ✅ | ✅
|
||||
- Map scroll | ? | ?
|
||||
- Compass | ? | 2
|
||||
- Scan | ? | ?
|
||||
- Lockbox | ? | ?
|
||||
- Celtic knot | ? | ✅
|
||||
- Slide | ? | ✅
|
||||
- Towers | ? | ?
|
||||
Example app | ✅ | ✅
|
||||
**AfkWarden** ||
|
||||
- Inactive | ❌ | ✅
|
||||
- XP Counter | ❌ | ❌
|
||||
- Chatbox | 1 | ✅
|
||||
- Crafting menu | ✅ | ✅
|
||||
- Buffs | ✅ | ✅
|
||||
- Action bar stats | ✅ | ✅
|
||||
- Dialog box | ✅ | ✅
|
||||
- Target death | ? | ?
|
||||
- Sheathe stance | ? | ?
|
||||
- Castle Wars | ? | ?
|
||||
- Fight kiln waves | ? | ?
|
||||
- Target death | ? | ?
|
||||
- Item drops | ? | ?
|
||||
**DgKey** ||
|
||||
- Show map | ✅ | ?
|
||||
- Track key | ❌ | ?
|
||||
- Select player location | ❌ | ?
|
||||
**[ArchMatCounter](https://zerogwafa.github.io/ArchMatCounter/appconfig.json)** | ? | ?
|
||||
### Linux
|
||||
|
||||
- 1: Can be setup but only fire once (needs inactive to work)
|
||||
- 2: Detected but cannot place mark
|
||||
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
|
||||
|
||||
## Running
|
||||
### 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
|
||||
|
||||
This project use [Poetry](https://python-poetry.org) as package manager.
|
||||
|
||||
@@ -53,60 +44,34 @@ Requires Poetry 1.1.
|
||||
|
||||
```sh
|
||||
poetry install
|
||||
poetry build
|
||||
cp build/lib.*/runekit/image/*.cpython*.* runekit/image/
|
||||
# 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 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
|
||||
```
|
||||
|
||||
### Linux additional instruction
|
||||
|
||||
Requires libxcb to be installed
|
||||
|
||||
### macOS additional instruction
|
||||
|
||||
You'll probably need Command Line Tools to build the C code, run: `xcode-select --install` to get it
|
||||
|
||||
You will need to add Python in System Preferences > Security > Privacy in these sections:
|
||||
- Accessibility
|
||||
- Screen Recording
|
||||
|
||||
Note that Python might appear as the closest macOS application (eg. your terminal emulator) instead of Python
|
||||
|
||||
For best result, the game window should be on non-Retina display
|
||||
|
||||
## Developer
|
||||
|
||||
Start with `--remote-debugging-port=9222` to enable remote debugger protocol.
|
||||
To debug, go to `chrome://inspect` on Chrome/Chromium.
|
||||
|
||||
## Technical Features
|
||||
### Building .app on Mac
|
||||
|
||||
* [Alt1 Compatibility](compatibility.md)
|
||||
|
||||
Platform Feature | Linux | macOS
|
||||
------------------------------|-------|--------
|
||||
**Game Manager** ||
|
||||
Instance change signals | ❌ | ❌
|
||||
**Instance** ||
|
||||
alt1_pressed | ❌ | ✅
|
||||
game_activity | ❌ | ✅
|
||||
positionChanged | ❌ | ✅
|
||||
scalingChanged | ❌ | ❌
|
||||
focusChanged | ✅ | ✅
|
||||
set_taskbar_progress | ❌ | ❌
|
||||
Window capture | ✅ | ✅
|
||||
|
||||
## Known issues
|
||||
|
||||
- On macOS some values include the size of window decorator, including the screenshot
|
||||
- This can be tested with AfkScape color picker
|
||||
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 protect is [No Maintenance Intended](https://unmaintained.tech/).
|
||||
This project 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, making it a proper open source project is an ongoing work that I'm not willing to do.
|
||||
However, maintaining it as a proper open source project is an ongoing work that I don't have the bandwidth 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.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- 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'
|
||||
}
|
||||
})
|
||||
@@ -1,18 +0,0 @@
|
||||
from Cython.Build import cythonize
|
||||
|
||||
# use cythonize to build the extensions
|
||||
modules = [
|
||||
"runekit/image/format.pyx",
|
||||
]
|
||||
|
||||
extensions = cythonize(modules)
|
||||
|
||||
|
||||
def build(setup_kwargs):
|
||||
"""Needed for the poetry building interface."""
|
||||
|
||||
setup_kwargs.update(
|
||||
{
|
||||
"ext_modules": extensions,
|
||||
}
|
||||
)
|
||||
@@ -3,8 +3,8 @@
|
||||
- [ ] alt1.overlay
|
||||
- [ ] alt1.captureMethod
|
||||
- [ ] alt1.permission*
|
||||
- [ ] alt1.lastWorldHop
|
||||
- [ ] alt1.currentWorld
|
||||
- [x] alt1.lastWorldHop
|
||||
- [x] alt1.currentWorld
|
||||
- [ ] alt1.rsPing
|
||||
- [ ] alt1.rsFps
|
||||
- [ ] alt1.openInfo
|
||||
@@ -16,26 +16,27 @@
|
||||
- [x] alt1.rsActive
|
||||
- [x] alt1.userResize
|
||||
- [ ] alt1.identiyAppUrl
|
||||
- [ ] alt1.openBrowser
|
||||
- [ ] alt1.clearBinds
|
||||
- [x] alt1.openBrowser
|
||||
- [x] alt1.clearBinds
|
||||
- [ ] alt1.registerStatusDaemon
|
||||
- [ ] alt1.getStatusDaemonState
|
||||
- [ ] alt1.showNotification
|
||||
- [ ] alt1.closeApp
|
||||
- [x] alt1.showNotification
|
||||
- [x] alt1.closeApp
|
||||
- [x] alt1.setTooltip
|
||||
- [x] alt1.clearTooltip
|
||||
- [x] alt1.setTaskbarProgress
|
||||
- [ ] alt1.setTitleBarText - Implemented but not validated
|
||||
- [ ] alt1.overLayRect
|
||||
- [ ] alt1.overLayTextEx
|
||||
- [ ] alt1.overLayLine
|
||||
- [ ] alt1.overLayImage
|
||||
- [ ] alt1.overLayClearGroup
|
||||
- [ ] alt1.overLaySetGroup
|
||||
- [ ] alt1.overLayFreezeGroup
|
||||
- [ ] alt1.overLayContinueGroup
|
||||
- [ ] alt1.overLayRefreshGroup
|
||||
- [ ] alt1.overLaySetGroupZIndex
|
||||
- [ ] 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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>runekit</id>
|
||||
<metadata_license>GPLv3</metadata_license>
|
||||
<project_license>GPLv3</project_license>
|
||||
<name>RuneKit</name>
|
||||
<description>
|
||||
<p>Alt1-compatible toolbox for RuneScape 3</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">RuneKit.desktop</launchable>
|
||||
<url type="homepage">https://github.com/whs/runekit</url>
|
||||
<provides>
|
||||
<binary>runekit</binary>
|
||||
<python3>runekit</python3>
|
||||
</provides>
|
||||
</component>
|
||||
@@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=RuneKit
|
||||
Exec=runekit
|
||||
Comment=Alt1-compatible toolbox for RuneScape 3
|
||||
Icon=python
|
||||
Categories=Game;
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Export APPRUN if running from an extracted image
|
||||
self="$(readlink -f -- $0)"
|
||||
here="${self%/*}"
|
||||
APPDIR="${APPDIR:-${here}}"
|
||||
|
||||
# Export SSL certificate
|
||||
export SSL_CERT_FILE="${APPDIR}/opt/_internal/certs.pem"
|
||||
export PYTHONHOME="${APPDIR}/opt/python3.9"
|
||||
|
||||
exec $APPDIR/usr/bin/runekit $@
|
||||
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,75 @@
|
||||
# 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,44 +1,4 @@
|
||||
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)
|
||||
|
||||
from runekit.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,3 +1,11 @@
|
||||
[[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"
|
||||
@@ -30,7 +38,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2020.12.5"
|
||||
version = "2021.10.8"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -38,7 +46,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.14.4"
|
||||
version = "1.14.6"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -48,36 +56,53 @@ python-versions = "*"
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "4.0.0"
|
||||
description = "Universal encoding detector for Python 2 and 3"
|
||||
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 = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "7.1.2"
|
||||
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."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "cython"
|
||||
version = "0.29.21"
|
||||
description = "The Cython compiler for writing C extensions for the Python language."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "2.10"
|
||||
version = "3.2"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
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"
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
@@ -87,22 +112,52 @@ 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.8.1"
|
||||
version = "0.9.0"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "8.1.0"
|
||||
version = "8.3.2"
|
||||
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"
|
||||
@@ -111,9 +166,34 @@ 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.1"
|
||||
version = "7.3"
|
||||
description = "Python<->ObjC Interoperability Module"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -121,52 +201,52 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-applicationservices"
|
||||
version = "7.1"
|
||||
version = "7.3"
|
||||
description = "Wrappers for the framework ApplicationServices on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.1"
|
||||
pyobjc-framework-Cocoa = ">=7.1"
|
||||
pyobjc-framework-Quartz = ">=7.1"
|
||||
pyobjc-core = ">=7.3"
|
||||
pyobjc-framework-Cocoa = ">=7.3"
|
||||
pyobjc-framework-Quartz = ">=7.3"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "7.1"
|
||||
version = "7.3"
|
||||
description = "Wrappers for the Cocoa frameworks on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.1"
|
||||
pyobjc-core = ">=7.3"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-coretext"
|
||||
version = "7.1"
|
||||
version = "7.3"
|
||||
description = "Wrappers for the framework CoreText on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.1"
|
||||
pyobjc-framework-Cocoa = ">=7.1"
|
||||
pyobjc-framework-Quartz = ">=7.1"
|
||||
pyobjc-core = ">=7.3"
|
||||
pyobjc-framework-Cocoa = ">=7.3"
|
||||
pyobjc-framework-Quartz = ">=7.3"
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-quartz"
|
||||
version = "7.1"
|
||||
version = "7.3"
|
||||
description = "Wrappers for the Quartz frameworks on macOS"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyobjc-core = ">=7.1"
|
||||
pyobjc-framework-Cocoa = ">=7.1"
|
||||
pyobjc-core = ">=7.3"
|
||||
pyobjc-framework-Cocoa = ">=7.3"
|
||||
|
||||
[[package]]
|
||||
name = "pyside2"
|
||||
@@ -181,7 +261,7 @@ shiboken2 = "5.15.2"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2020.11.13"
|
||||
version = "2021.10.8"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -189,21 +269,21 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.25.1"
|
||||
version = "2.26.0"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
chardet = ">=3.0.2,<5"
|
||||
idna = ">=2.5,<3"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"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"
|
||||
@@ -215,7 +295,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.10"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -239,7 +319,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -247,7 +327,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.7.4.3"
|
||||
version = "3.10.0.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -255,7 +335,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.3"
|
||||
version = "1.26.7"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -281,9 +361,13 @@ six = "*"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9,<3.10"
|
||||
content-hash = "7798946c9dfae93142a329e7abe1bf4398c9c631bf0a19a0775b56db7809f949"
|
||||
content-hash = "6d7ef5bf604ccfd42e5b3251cf4e6bc5e1ee5d89294bb39f72f984abeb1dd43f"
|
||||
|
||||
[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"},
|
||||
@@ -292,177 +376,283 @@ black = [
|
||||
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
|
||||
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"},
|
||||
{file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"},
|
||||
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"},
|
||||
{file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"},
|
||||
{file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"},
|
||||
{file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"},
|
||||
{file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"},
|
||||
{file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"},
|
||||
{file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"},
|
||||
{file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"},
|
||||
{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"},
|
||||
]
|
||||
chardet = [
|
||||
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
||||
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
|
||||
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"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||
{file = "click-8.0.2-py3-none-any.whl", hash = "sha256:3fab8aeb8f15f5452ae7511ad448977b3417325bceddd53df87e0bb81f3a8cf8"},
|
||||
{file = "click-8.0.2.tar.gz", hash = "sha256:7027bc7bbafaab8b2c2816861d8eb372429ee3c02e193fc2f93d6c4ab9de49c5"},
|
||||
]
|
||||
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"},
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
||||
{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"},
|
||||
]
|
||||
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.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
|
||||
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-win32.whl", hash = "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"},
|
||||
{file = "Pillow-8.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-win32.whl", hash = "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e"},
|
||||
{file = "Pillow-8.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-win32.whl", hash = "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59"},
|
||||
{file = "Pillow-8.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-win32.whl", hash = "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b"},
|
||||
{file = "Pillow-8.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865"},
|
||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9"},
|
||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913"},
|
||||
{file = "Pillow-8.1.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820"},
|
||||
{file = "Pillow-8.1.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5"},
|
||||
{file = "Pillow-8.1.0.tar.gz", hash = "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba"},
|
||||
{file = "Pillow-8.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"},
|
||||
]
|
||||
pyobjc-core = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
pyobjc-framework-applicationservices = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
pyobjc-framework-cocoa = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
pyobjc-framework-coretext = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
pyobjc-framework-quartz = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
pyside2 = [
|
||||
{file = "PySide2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:4f17a0161995678110447711d685fcd7b15b762810e8f00f6dc239bffb70a32e"},
|
||||
@@ -473,51 +663,51 @@ pyside2 = [
|
||||
{file = "PySide2-5.15.2-5.15.2-cp35.cp36.cp37.cp38.cp39-none-win_amd64.whl", hash = "sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
|
||||
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
|
||||
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
|
||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||
]
|
||||
shiboken2 = [
|
||||
{file = "shiboken2-5.15.2-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:03f41b0693b91c7f89627f1085a4ecbe8591c03f904118a034854d935e0e766c"},
|
||||
@@ -528,8 +718,8 @@ 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.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
{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"},
|
||||
@@ -543,45 +733,45 @@ toml = [
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
typed-ast = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
|
||||
{file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
|
||||
{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"},
|
||||
|
||||
@@ -4,26 +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.25.1"
|
||||
requests = "^2.26.0"
|
||||
PySide2 = "^5.15.2"
|
||||
Pillow = "^8.1.0"
|
||||
Pillow = "^8.3.1"
|
||||
sysv-ipc = [{ version="^1.1.0", platform="linux" }]
|
||||
xcffib = [{ version="^0.11.1", 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"
|
||||
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"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
Cython = "^0.29.21"
|
||||
black = "^20.8b1"
|
||||
pyinstaller = [{ version="^4.5.1", platform="darwin" }]
|
||||
|
||||
[tool.poetry.build]
|
||||
script = "build.py"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.1.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
[tool.poetry.scripts]
|
||||
runekit = 'runekit.main:main'
|
||||
@@ -0,0 +1,22 @@
|
||||
<!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>
|
||||
@@ -25,3 +25,13 @@ class RequestHandler(typing.TypedDict):
|
||||
handlerName: str
|
||||
handlerUrl: str
|
||||
handlerScript: str
|
||||
|
||||
|
||||
class RectLike(typing.TypedDict):
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
CaptureMulti = typing.Dict[str, typing.Optional[RectLike]]
|
||||
@@ -5,7 +5,7 @@ import requests
|
||||
from runekit.alt1.schema import AppManifest
|
||||
|
||||
|
||||
def fetch_manifest(url: str) -> AppManifest:
|
||||
def fetch_bom_json(url: str):
|
||||
req = requests.get(url)
|
||||
req.raise_for_status()
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .app import App
|
||||
from .store import AppStore
|
||||
@@ -13,20 +13,38 @@ 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, manifest, game_instance, source_url=None):
|
||||
def __init__(self, host, app_id, 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:
|
||||
@@ -49,6 +67,7 @@ class App:
|
||||
|
||||
@property
|
||||
def absolute_app_url(self) -> str:
|
||||
# TODO: Remove
|
||||
if self.source_url:
|
||||
return urljoin(self.source_url, self.manifest["appUrl"])
|
||||
|
||||
@@ -56,6 +75,7 @@ class App:
|
||||
|
||||
@property
|
||||
def absolute_config_url(self) -> str:
|
||||
# TODO: Remove
|
||||
if self.source_url:
|
||||
return urljoin(self.source_url, self.manifest["configUrl"])
|
||||
|
||||
@@ -63,6 +83,7 @@ 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"])
|
||||
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
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,37 +1,101 @@
|
||||
import typing
|
||||
import logging
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import Qt, Slot, QRect
|
||||
from PySide2.QtGui import QIcon
|
||||
from PySide2.QtCore import Qt, Slot, QRect, QObject, QUrl
|
||||
from PySide2.QtGui import QIcon, QDesktopServices
|
||||
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 BrowserWindow(QMainWindow):
|
||||
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):
|
||||
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)
|
||||
|
||||
page = QWebEnginePage(self.app.get_web_profile(), 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.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)
|
||||
self.browser.setPage(page)
|
||||
page.featurePermissionRequested.connect(self.on_permission_request)
|
||||
return page
|
||||
|
||||
@Slot(QRect)
|
||||
def on_geometry_change(self, size: QRect):
|
||||
@@ -45,13 +109,50 @@ class BrowserWindow(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 = typing.cast(BrowserWindow, self.parent())
|
||||
app_window = self.browser_window()
|
||||
popup = PopupWindow(app=app_window.app, parent=app_window)
|
||||
popup.setWindowIcon(app_window.windowIcon())
|
||||
popup.show()
|
||||
|
||||
def show(ok: bool):
|
||||
popup.browser.loadFinished.disconnect(show)
|
||||
if ok:
|
||||
popup.show()
|
||||
else:
|
||||
popup.close()
|
||||
|
||||
popup.browser.loadFinished.connect(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
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from PySide2.QtCore import QSize, Qt, QRunnable, Slot, QObject, Signal, QThreadPool
|
||||
from PySide2.QtGui import QIcon, QPixmap
|
||||
from PySide2.QtCore import QSize, Qt, QThreadPool, QSettings
|
||||
from PySide2.QtGui import QIcon
|
||||
|
||||
from runekit.app.view.browser_window import BrowserWindow
|
||||
|
||||
@@ -14,16 +14,19 @@ 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?
|
||||
super().__init__(
|
||||
flags=Qt.WindowTitleHint
|
||||
| Qt.WindowCloseButtonHint
|
||||
| Qt.NoDropShadowWindowHint
|
||||
| Qt.WindowStaysOnTopHint,
|
||||
**kwargs
|
||||
)
|
||||
flags = Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint | Qt.Window
|
||||
if self.framed:
|
||||
flags |= Qt.CustomizeWindowHint
|
||||
self.setWindowFlags(flags)
|
||||
|
||||
self.pool = QThreadPool(parent=self)
|
||||
self.setWindowTitle(self.app.manifest["appName"])
|
||||
self.snap_to_game()
|
||||
|
||||
self.logger = logging.getLogger(
|
||||
__name__
|
||||
@@ -34,20 +37,14 @@ class AppWindow(BrowserWindow):
|
||||
)
|
||||
|
||||
self.browser.load(self.app.absolute_app_url)
|
||||
self._update_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)
|
||||
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(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)
|
||||
@property
|
||||
def framed(self) -> bool:
|
||||
return self.settings.value("settings/styledBorder", "true") == "true" and sys.platform != "darwin"
|
||||
|
||||
def minimumSize(self) -> QSize:
|
||||
return QSize(self.app.manifest["minWidth"], self.app.manifest["minHeight"])
|
||||
@@ -59,22 +56,3 @@ 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,7 +1,10 @@
|
||||
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,6 +8,20 @@
|
||||
// (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;
|
||||
@@ -20,15 +34,25 @@
|
||||
});
|
||||
};
|
||||
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);
|
||||
|
||||
@@ -38,6 +62,25 @@
|
||||
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++){
|
||||
@@ -50,11 +93,12 @@
|
||||
}
|
||||
|
||||
let lastGameActivity = window.performance.now();
|
||||
let drawCallId = 0;
|
||||
|
||||
window.alt1 = {
|
||||
overlay: {},
|
||||
version: '1.1.1',
|
||||
versionint: 1001001,
|
||||
version: '1.2.0',
|
||||
versionint: 1002000,
|
||||
maxtransfer: 4000000, // also hardcoded in lib...
|
||||
skinName: '',
|
||||
captureMethod: '',
|
||||
@@ -63,7 +107,6 @@
|
||||
permissionOverlay: true,
|
||||
permissionPixel: true,
|
||||
lastWorldHop: 0,
|
||||
currentWorld: -1,
|
||||
rsPing: 0,
|
||||
rsFps: 60,
|
||||
openInfo: '{}',
|
||||
@@ -147,6 +190,11 @@
|
||||
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);
|
||||
},
|
||||
@@ -160,11 +208,13 @@
|
||||
});
|
||||
},
|
||||
|
||||
// openBrowser(url) {
|
||||
// },
|
||||
openBrowser(url) {
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
// clearBinds() {
|
||||
// },
|
||||
clearBinds() {
|
||||
window.alt1.clearTooltip();
|
||||
},
|
||||
|
||||
// registerStatusDaemon(serverUrl, state) {
|
||||
// },
|
||||
@@ -172,11 +222,15 @@
|
||||
// return '';
|
||||
// },
|
||||
|
||||
// showNotification() {
|
||||
// },
|
||||
showNotification(title, message, icon) {
|
||||
// FIXME: Just use HTML5 Notification once QWebEngineNotification is implemented
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.showNotification(title, message, icon);
|
||||
});
|
||||
},
|
||||
|
||||
closeApp() {
|
||||
window.close()
|
||||
window.close();
|
||||
},
|
||||
|
||||
setTooltip(tooltip) {
|
||||
@@ -197,42 +251,71 @@
|
||||
},
|
||||
|
||||
setTitleBarText(text) {
|
||||
document.title = text;
|
||||
|
||||
},
|
||||
|
||||
overLayRect(color, x, y, w, h, time, lineWidth) {
|
||||
console.trace('overLayRect');
|
||||
return false;
|
||||
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);
|
||||
},
|
||||
overLayTextEx(str, color, size, x, y, time, fontname, centered, shadow) {
|
||||
console.trace('overLayTextEx');
|
||||
return false;
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayTextEx(drawCallId++, str, color, size, x, y, time, fontname, centered, shadow);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLayLine(color, width, x1, y1, x2, y2, time) {
|
||||
console.trace('overLayLine');
|
||||
return false;
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayLine(drawCallId++, color, width, x1, y1, x2, y2, time);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLayImage(x, y, imgstr, imgwidth, time) {
|
||||
console.trace('overLayImage');
|
||||
return false;
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayImage(drawCallId++, x, y, imgstr, imgwidth, time);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLayClearGroup(group) {
|
||||
console.trace('overLayClearGroup');
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayClearGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLaySetGroup(group) {
|
||||
console.trace('overLaySetGroup');
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlaySetGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLayFreezeGroup(group) {
|
||||
console.trace('overLayFreezeGroup');
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayFreezeGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLayContinueGroup(group) {
|
||||
console.trace('overLayContinueGroup');
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayContinueGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLayRefreshGroup(group) {
|
||||
console.trace('overLayRefreshGroup');
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlayRefreshGroup(drawCallId++, group);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
overLaySetGroupZIndex(groupname, zIndex) {
|
||||
console.trace('overLaySetGroupZIndex');
|
||||
overLaySetGroupZIndex(group, zIndex) {
|
||||
channel.then(function(chan){
|
||||
chan.objects.alt1.overlaySetGroupZIndex(drawCallId++, group, zIndex);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
||||
getRegion(x, y, w, h) {
|
||||
@@ -268,7 +351,31 @@
|
||||
// },
|
||||
// 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){
|
||||
@@ -287,24 +394,26 @@
|
||||
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,
|
||||
},
|
||||
mouseRs: {
|
||||
x: mouseRsX,
|
||||
y: mouseRsY,
|
||||
},
|
||||
x: mouseRsX,
|
||||
y: mouseRsY,
|
||||
x: mouseX,
|
||||
y: mouseY,
|
||||
rsLinked: true,
|
||||
};
|
||||
emit(event);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import secrets
|
||||
from typing import TYPE_CHECKING, Dict, Callable, List
|
||||
from typing import TYPE_CHECKING, Dict, Callable, List, NamedTuple
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from PIL import Image
|
||||
from PySide2.QtCore import (
|
||||
QObject,
|
||||
Slot,
|
||||
@@ -24,44 +22,22 @@ 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 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 BoundedRegion(NamedTuple):
|
||||
image: ImageType
|
||||
|
||||
|
||||
class Alt1Api(QObject):
|
||||
@@ -71,20 +47,31 @@ class Alt1Api(QObject):
|
||||
alt1Signal = Signal(int)
|
||||
|
||||
_screen_info: QRect
|
||||
_bound_regions: List
|
||||
_bound_regions: List[BoundedRegion]
|
||||
_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()
|
||||
@@ -92,6 +79,7 @@ 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)
|
||||
@@ -113,6 +101,7 @@ class Alt1Api(QObject):
|
||||
self.screen_update_signal.emit()
|
||||
|
||||
screen_update_signal = Signal()
|
||||
|
||||
# endregion
|
||||
|
||||
# region Qt Properties
|
||||
@@ -148,9 +137,9 @@ class Alt1Api(QObject):
|
||||
return 0
|
||||
|
||||
value = QCursor.pos()
|
||||
pos = self.app.game_instance.get_position()
|
||||
pos = self._game_position
|
||||
|
||||
if not pos.contains(value, True):
|
||||
if not pos.contains(value, True) or not self._game_active:
|
||||
# Cursor is out of game
|
||||
return 0
|
||||
|
||||
@@ -203,6 +192,12 @@ 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
|
||||
@@ -210,14 +205,25 @@ class Alt1Api(QObject):
|
||||
if not self.app.has_permission("pixel"):
|
||||
raise ApiPermissionDeniedException("pixel")
|
||||
|
||||
return _image_to_stream(self.app.game_instance.grab_region(x, y, w, h))
|
||||
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"
|
||||
)
|
||||
|
||||
def bind_region(self, x, y, w, h):
|
||||
if not self.app.has_permission("pixel"):
|
||||
return 0
|
||||
|
||||
# Alt1 only support one region
|
||||
self._bound_regions = [self.app.game_instance.grab_region(x, y, w, h)]
|
||||
bound_data = self.app.game_instance.grab_region(x, y, w, h)
|
||||
self._bound_regions = [BoundedRegion(image=bound_data)]
|
||||
return 1
|
||||
|
||||
def bind_get_region(self, id, x, y, w, h):
|
||||
@@ -230,10 +236,25 @@ class Alt1Api(QObject):
|
||||
try:
|
||||
image = self._bound_regions[id - 1]
|
||||
except IndexError:
|
||||
print("no img index %d" % id)
|
||||
self.logger.warning("bindGetRegion(%d) but image not bound", id)
|
||||
return ""
|
||||
|
||||
return _image_to_stream(image, x, y, w, h)
|
||||
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)
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -245,6 +266,13 @@ 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"""
|
||||
@@ -268,6 +296,97 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
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,9 +1,12 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import QFile, QIODevice
|
||||
from PySide2.QtWebEngineWidgets import QWebEngineProfile, QWebEngineScript
|
||||
from PySide2.QtWebEngineWidgets import (
|
||||
QWebEngineProfile,
|
||||
QWebEngineScript,
|
||||
QWebEngineSettings,
|
||||
)
|
||||
|
||||
from runekit.browser.api import RuneKitSchemeHandler
|
||||
from runekit.browser.scheme import RuneKitScheme
|
||||
@@ -11,25 +14,23 @@ 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):
|
||||
# TODO: Persist
|
||||
super().__init__(parent)
|
||||
super().__init__("app", parent=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(
|
||||
@@ -42,23 +43,25 @@ class WebProfile(QWebEngineProfile):
|
||||
)
|
||||
|
||||
def _insert_alt1_api(self):
|
||||
global _added_qwebchannel, src
|
||||
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()
|
||||
|
||||
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()
|
||||
src += "\n;;\n"
|
||||
|
||||
src = qwc_src + "\n;;\n" + src
|
||||
|
||||
_added_qwebchannel = True
|
||||
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()
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,107 @@
|
||||
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,15 +1,20 @@
|
||||
import abc
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from PySide2.QtCore import QObject, Signal, Property, QRect, Slot
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Union
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
@@ -56,17 +61,33 @@ class GameInstance(QObject):
|
||||
focused = Property(bool, is_focused, notify=focusChanged)
|
||||
|
||||
@abc.abstractmethod
|
||||
def grab_game(self) -> Image:
|
||||
def get_world(self) -> Optional[int]:
|
||||
...
|
||||
|
||||
worldChanged = Signal(int)
|
||||
world = Property(int, get_world, notify=worldChanged)
|
||||
|
||||
@abc.abstractmethod
|
||||
def grab_game(self) -> ImageType:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def grab_desktop(self, x: int, y: int, w: int, h: int) -> Image:
|
||||
def grab_desktop(self, x: int, y: int, w: int, h: int) -> ImageType:
|
||||
...
|
||||
|
||||
def grab_region(self, x: int, y: int, w: int, h: int) -> Image:
|
||||
def grab_region(self, x: int, y: int, w: int, h: int) -> ImageType:
|
||||
image = self.grab_game()
|
||||
return image.crop((x, y, x + w, y + h))
|
||||
|
||||
@abc.abstractmethod
|
||||
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
|
||||
@@ -1,5 +1,5 @@
|
||||
import abc
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
from PySide2.QtCore import QObject, Property, Signal
|
||||
|
||||
@@ -12,7 +12,15 @@ class GameManager(QObject):
|
||||
"""Return a list of active game instance. The instances returned should be stable (same instance for all invocation)"""
|
||||
...
|
||||
|
||||
instance_added = Signal()
|
||||
instance_removed = Signal()
|
||||
@abc.abstractmethod
|
||||
def get_active_instance(self) -> Union[GameInstance, None]:
|
||||
...
|
||||
|
||||
instance_added = Signal(GameInstance)
|
||||
instance_removed = Signal(GameInstance)
|
||||
instance_changed = Signal()
|
||||
instances = Property(list, get_instances, notify=instance_changed)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the GameManager and any GameInstances"""
|
||||
pass
|
||||
@@ -0,0 +1,108 @@
|
||||
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()
|
||||
@@ -0,0 +1,94 @@
|
||||
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,13 +1,37 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from PIL import Image
|
||||
from PySide2.QtGui import QGuiApplication, QWindow
|
||||
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
|
||||
|
||||
_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
|
||||
|
||||
@@ -18,29 +42,34 @@ class QtGrabMixin(QtBaseMixin):
|
||||
__game_last_grab = 0.0
|
||||
__game_last_image = None
|
||||
|
||||
def grab_game(self) -> Image:
|
||||
def grab_game(self) -> np.ndarray:
|
||||
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 = Image.fromqpixmap(pixmap)
|
||||
image = qpixmap_to_np(pixmap)
|
||||
|
||||
if _debug_dump_file:
|
||||
logger.debug("dumping file")
|
||||
pixmap.save("/tmp/qtshot.bmp", None, 100)
|
||||
np_save_image(image, "/tmp/qtshot.bmp")
|
||||
|
||||
self.__game_last_image = image
|
||||
self.__game_last_grab = time.monotonic()
|
||||
return self.__game_last_image
|
||||
|
||||
def grab_desktop(self, x, y, w, h) -> Image:
|
||||
def grab_desktop(self, x, y, w, h) -> np.ndarray:
|
||||
screen = QGuiApplication.primaryScreen()
|
||||
pixmap = screen.grabWindow(0, x, y, w, h)
|
||||
image = Image.fromqpixmap(pixmap)
|
||||
image = qpixmap_to_np(pixmap)
|
||||
|
||||
if _debug_dump_file:
|
||||
logger.debug("dumping file")
|
||||
pixmap.save("/tmp/qtshot.bmp", None, 100)
|
||||
np_save_image(image, "/tmp/qtshot.bmp")
|
||||
|
||||
return image
|
||||
|
||||
|
||||
class QtEmbedMixin(QtBaseMixin):
|
||||
def embed_window(self, window: QWindow):
|
||||
window.setParent(self.qwindow)
|
||||
@@ -1,6 +1,7 @@
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import Quartz
|
||||
import objc
|
||||
@@ -8,8 +9,13 @@ 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__)
|
||||
@@ -71,13 +77,14 @@ def on_ax_event(observer, element, notification, ptr):
|
||||
self.logger.warning("Got unknown AX event %s", notification)
|
||||
|
||||
|
||||
class QuartzGameInstance(GameInstance):
|
||||
class QuartzGameInstance(PsUtilNetStat, GameInstance):
|
||||
_is_active = False
|
||||
overlay: QGraphicsItem
|
||||
|
||||
__game_last_grab = 0.0
|
||||
__game_last_image = None
|
||||
|
||||
def __init__(self, manager, wid, pid, **kwargs):
|
||||
def __init__(self, manager: "QuartzGameManager", wid, pid, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.manager = manager
|
||||
self.wid = wid
|
||||
@@ -87,6 +94,7 @@ class QuartzGameInstance(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)
|
||||
@@ -123,6 +131,7 @@ class QuartzGameInstance(GameInstance):
|
||||
)
|
||||
|
||||
objc.context.unregister(self)
|
||||
self._overlay_disconnect()
|
||||
|
||||
def get_position(self) -> QRect:
|
||||
# The docs say this API is expensive...
|
||||
@@ -155,6 +164,10 @@ class QuartzGameInstance(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:
|
||||
@@ -176,8 +189,8 @@ class QuartzGameInstance(GameInstance):
|
||||
out = cgimageref_to_image(imgref)
|
||||
return out.resize((w, h), Image.NEAREST)
|
||||
|
||||
def set_taskbar_progress(self, type, progress):
|
||||
pass
|
||||
def get_overlay_area(self) -> QGraphicsItem:
|
||||
return self.overlay
|
||||
|
||||
|
||||
class AXAPIError(Exception):
|
||||
@@ -188,6 +201,7 @@ 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):
|
||||
@@ -195,3 +209,8 @@ 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,12 +1,15 @@
|
||||
import time
|
||||
from functools import reduce
|
||||
from typing import List, Dict, Optional
|
||||
from typing import List, Dict, Optional, Union
|
||||
|
||||
import Quartz
|
||||
from PySide2.QtCore import QTimer
|
||||
import ApplicationServices
|
||||
from PySide2.QtCore import QTimer, Signal, Slot
|
||||
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
|
||||
|
||||
@@ -15,12 +18,26 @@ 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,
|
||||
@@ -42,6 +59,24 @@ 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
|
||||
@@ -58,6 +93,12 @@ class QuartzGameManager(GameManager):
|
||||
|
||||
return list(self._instances.values())
|
||||
|
||||
def get_active_instance(self) -> Union[GameInstance, None]:
|
||||
if not self._instances:
|
||||
return None
|
||||
|
||||
return 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:
|
||||
@@ -78,8 +119,9 @@ 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:
|
||||
@@ -94,25 +136,22 @@ 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 Accessibility Access for global hotkey and game activity monitoring\n\nOpen System Preferences > Security > Privacy > Accessibility to allow this",
|
||||
"RuneKit needs Screen Recording permission\n\nOpen System Preferences > Security > Privacy > Screen Recording to allow this",
|
||||
QMessageBox.Open | QMessageBox.Ignore,
|
||||
)
|
||||
button = msgbox.exec()
|
||||
|
||||
if button == QMessageBox.Open:
|
||||
QDesktopServices.openUrl(
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Screen Recording"
|
||||
)
|
||||
|
||||
# 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,66 +0,0 @@
|
||||
import xcffib
|
||||
|
||||
opcode_cache = {}
|
||||
|
||||
|
||||
def get_extension_by_opcode(connection, opcode):
|
||||
if opcode in opcode_cache:
|
||||
return opcode_cache[opcode]
|
||||
|
||||
for name in connection.core.ListExtensions().reply().names:
|
||||
ext_opcode = (
|
||||
connection.core.QueryExtension(name.name_len, name.name)
|
||||
.reply()
|
||||
.major_opcode
|
||||
)
|
||||
opcode_cache[ext_opcode] = xcffib.ExtensionKey(name.name.to_string())
|
||||
|
||||
try:
|
||||
return opcode_cache[opcode]
|
||||
except KeyError:
|
||||
raise ExtensionNotFound(opcode)
|
||||
|
||||
|
||||
class ExtensionNotFound(Exception):
|
||||
def __init__(self, opcode):
|
||||
self.opcode = opcode
|
||||
super().__init__(f"Extension with opcode {opcode} was not found")
|
||||
|
||||
|
||||
class GeGenericEvent(xcffib.Event):
|
||||
def __init__(self, unpacker):
|
||||
if isinstance(unpacker, xcffib.Protobj):
|
||||
unpacker = xcffib.MemoryUnpacker(unpacker.pack())
|
||||
|
||||
super().__init__(unpacker)
|
||||
try:
|
||||
base = unpacker.offset
|
||||
(
|
||||
self.extension,
|
||||
self.sequence_number,
|
||||
self.length,
|
||||
self.evtype,
|
||||
) = unpacker.unpack("xBHIH")
|
||||
self.payload = xcffib.List(unpacker, "I", 8 + self.length)
|
||||
self.bufsize = unpacker.offset - base
|
||||
except:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
def to_event(self, connection):
|
||||
extension = get_extension_by_opcode(connection, self.extension)
|
||||
|
||||
constructor = xcffib.extensions[extension][1][self.evtype]
|
||||
unpacker = xcffib.MemoryUnpacker(self.payload.buf())
|
||||
try:
|
||||
return constructor(unpacker)
|
||||
except:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
|
||||
xcffib.core_events[35] = GeGenericEvent
|
||||
@@ -1,142 +1,122 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
|
||||
from PySide2.QtCore import QRect, Slot, Signal
|
||||
from PySide2.QtGui import QWindow, QGuiApplication
|
||||
import xcffib.xproto
|
||||
import xcffib.composite
|
||||
import xcffib.xinput
|
||||
from PIL import Image, ImageOps
|
||||
import xcffib.xproto
|
||||
from PySide2.QtCore import QRect, Signal, Slot
|
||||
from PySide2.QtGui import QWindow, QGuiApplication
|
||||
from PySide2.QtWidgets import QGraphicsItem
|
||||
|
||||
from runekit.game.instance import GameInstance
|
||||
from runekit.game.qt import QtGrabMixin
|
||||
from runekit.game.psutil_mixins import PsUtilNetStat
|
||||
from runekit.game.qt import QtGrabMixin, QtEmbedMixin
|
||||
from .ximage import zpixmap_shm_to_image
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import X11GameManager
|
||||
|
||||
KEY_1 = 10
|
||||
|
||||
|
||||
class X11GameInstance(QtGrabMixin, GameInstance):
|
||||
class X11GameInstance(QtGrabMixin, QtEmbedMixin, PsUtilNetStat, GameInstance):
|
||||
wid: int
|
||||
refresh_rate = 100
|
||||
|
||||
manager: "X11GameManager"
|
||||
overlay: QGraphicsItem
|
||||
|
||||
game_last_grab = 0.0
|
||||
game_last_image = None
|
||||
embedded_windows: List[QWindow]
|
||||
cached_position = None
|
||||
|
||||
key_press = Signal(xcffib.xinput.KeyPressEvent)
|
||||
input_signal = Signal(xcffib.Event)
|
||||
config_signal = Signal(xcffib.Event)
|
||||
destroy_signal = Signal(xcffib.Event)
|
||||
|
||||
def __init__(self, manager: "X11GameManager", wid: int, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
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._setup()
|
||||
self.key_press.connect(self.on_key_press)
|
||||
self.embedded_windows = []
|
||||
self._setup_x()
|
||||
self._setup_overlay()
|
||||
|
||||
self.input_signal.connect(self.on_input)
|
||||
self.config_signal.connect(self.on_config)
|
||||
|
||||
self._update_is_focused()
|
||||
|
||||
def _setup(self):
|
||||
def _setup_x(self):
|
||||
self.manager.xcomposite.RedirectWindow(
|
||||
self.wid, xcffib.composite.Redirect.Automatic
|
||||
)
|
||||
self.pixmap_id = self.manager.connection.generate_id()
|
||||
self.manager.xcomposite.NameWindowPixmap(
|
||||
self.wid, self.pixmap_id, is_checked=True
|
||||
)
|
||||
|
||||
self._grab_alt1()
|
||||
# self.manager.xinput.XIGrabDevice(
|
||||
# self.wid,
|
||||
# xcffib.xproto.Time.CurrentTime,
|
||||
# xcffib.xproto.Cursor._None,
|
||||
# xcffib.xinput.Device.All,
|
||||
# xcffib.xinput.GrabMode22.Async,
|
||||
# xcffib.xinput.GrabMode22.Async,
|
||||
# True,
|
||||
# 1,
|
||||
# (
|
||||
# xcffib.xinput.XIEventMask.KeyPress
|
||||
# | xcffib.xinput.XIEventMask.ButtonPress,
|
||||
# ),
|
||||
# )
|
||||
|
||||
def _grab_alt1(self):
|
||||
self.manager.xinput.XIPassiveGrabDevice(
|
||||
xcffib.xproto.Time.CurrentTime,
|
||||
self.name_pixmap()
|
||||
self.manager.connection.core.ChangeWindowAttributesChecked(
|
||||
self.wid,
|
||||
xcffib.xproto.Cursor._None,
|
||||
KEY_1,
|
||||
xcffib.xinput.Device.All,
|
||||
1,
|
||||
1,
|
||||
xcffib.xinput.GrabType.Keycode,
|
||||
xcffib.xinput.GrabMode22.Async,
|
||||
xcffib.xproto.GrabMode.Async,
|
||||
True,
|
||||
(
|
||||
xcffib.xinput.XIEventMask.KeyPress
|
||||
| xcffib.xinput.XIEventMask.KeyRelease,
|
||||
),
|
||||
(xcffib.xproto.KeyButMask.Mod1,),
|
||||
xcffib.xproto.CW.EventMask,
|
||||
[
|
||||
xcffib.xproto.EventMask.KeyPress
|
||||
| xcffib.xproto.EventMask.StructureNotify
|
||||
],
|
||||
)
|
||||
|
||||
def _ungrab_alt1(self):
|
||||
self.manager.xinput.XIPassiveUngrabDevice(
|
||||
self.wid,
|
||||
KEY_1,
|
||||
xcffib.xinput.Device.All,
|
||||
1,
|
||||
xcffib.xinput.GrabType.Keycode,
|
||||
(xcffib.xproto.KeyButMask.Mod1,),
|
||||
)
|
||||
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._ungrab_alt1()
|
||||
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:
|
||||
geom = self.manager.connection.core.GetGeometry(self.wid).reply()
|
||||
if self.cached_position:
|
||||
return self.cached_position
|
||||
|
||||
return QRect(geom.x, geom.y, geom.width, geom.height)
|
||||
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
|
||||
|
||||
def get_scaling(self) -> float:
|
||||
screen = QGuiApplication.screenAt(self.get_position().topLeft())
|
||||
pos = self.get_position().topLeft()
|
||||
if pos.x() < 0:
|
||||
pos.setX(0)
|
||||
if pos.y() < 0:
|
||||
pos.setY(0)
|
||||
|
||||
screen = QGuiApplication.screenAt(pos)
|
||||
return screen.devicePixelRatio()
|
||||
|
||||
def is_focused(self) -> bool:
|
||||
return self.is_focused
|
||||
return self._is_focused
|
||||
|
||||
def _update_is_focused(self):
|
||||
self.is_focused = self.manager.get_active_window() == self.wid
|
||||
self._is_focused = self.manager.get_active_window() == self.wid
|
||||
|
||||
def set_taskbar_progress(self, type_, progress):
|
||||
# TODO: Implement this on Unity? I don't use Unity
|
||||
# TODO: If progress = 1 set urgency flag
|
||||
pass
|
||||
|
||||
@Slot(xcffib.xinput.KeyPressEvent)
|
||||
def on_key_press(self, evt: xcffib.xinput.KeyPressEvent):
|
||||
# Check for alt1
|
||||
if evt.flags & xcffib.xinput.KeyEventFlags.KeyRepeat:
|
||||
return
|
||||
|
||||
if evt.mods.effective & xcffib.xproto.KeyButMask.Mod1 and evt.detail == KEY_1:
|
||||
if isinstance(evt, xcffib.xinput.KeyPressEvent):
|
||||
self.alt1_pressed.emit()
|
||||
self.logger.debug("alt1 pressed %s", repr(evt))
|
||||
else:
|
||||
self._ungrab_alt1()
|
||||
self._grab_alt1()
|
||||
|
||||
return
|
||||
|
||||
def grab_game(self):
|
||||
if (time.monotonic() - self.game_last_grab) * 1000 < self.refresh_rate:
|
||||
return self.game_last_image
|
||||
@@ -170,3 +150,31 @@ class X11GameInstance(QtGrabMixin, GameInstance):
|
||||
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,88 +1,117 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import List, Dict, Tuple
|
||||
import os
|
||||
import struct
|
||||
from typing import List, Dict, Tuple, Union
|
||||
|
||||
from PySide2.QtCore import QThread, Slot, Signal, QObject
|
||||
import sysv_ipc
|
||||
import xcffib
|
||||
import xcffib.xproto
|
||||
import xcffib.composite
|
||||
import xcffib.shm
|
||||
import xcffib.xinput
|
||||
import sysv_ipc
|
||||
import xcffib.xproto
|
||||
from PySide2.QtCore import QThread, Slot, QObject, Signal
|
||||
|
||||
from runekit.game import GameManager
|
||||
from .instance import GameInstance, X11GameInstance
|
||||
from . import ge
|
||||
from .utils import ClassDict
|
||||
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
|
||||
|
||||
_instance: Dict[int, X11GameInstance]
|
||||
_instances: Dict[int, X11GameInstance]
|
||||
_atom: Dict[bytes, int]
|
||||
_shm: List[Tuple[int, sysv_ipc.SharedMemory]]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(*kwargs)
|
||||
self._instance = {}
|
||||
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.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.on_active_window_changed.connect(
|
||||
self.on_active_window_changed
|
||||
)
|
||||
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):
|
||||
wm_class = self.get_property(wid, xcffib.xproto.Atom.WM_CLASS)
|
||||
if self.is_game(wid):
|
||||
if wid not in self._instances:
|
||||
self.logger.info("Found game instance %d", wid)
|
||||
instance = X11GameInstance(self, wid, parent=self)
|
||||
self._instances[wid] = instance
|
||||
|
||||
if wm_class:
|
||||
instance_name, app_name = wm_class.split("\00")
|
||||
if app_name == "RuneScape":
|
||||
if wid not in self._instance:
|
||||
instance = X11GameInstance(self, wid, parent=self)
|
||||
self._instance[wid] = instance
|
||||
|
||||
return
|
||||
|
||||
query = self.connection.core.QueryTree(wid).reply()
|
||||
for child in query.children:
|
||||
visit(child)
|
||||
try:
|
||||
query = self.connection.core.QueryTree(wid).reply()
|
||||
for child in query.children:
|
||||
visit(child)
|
||||
except xcffib.xproto.WindowError:
|
||||
return
|
||||
|
||||
visit(self.screen.root)
|
||||
|
||||
return list(self._instance.values())
|
||||
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")
|
||||
|
||||
@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.is_focused = active
|
||||
instance.focusChanged.emit(active)
|
||||
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)
|
||||
@@ -106,8 +135,8 @@ class X11GameManager(GameManager):
|
||||
|
||||
if reply.type == xcffib.xproto.Atom.STRING:
|
||||
return reply.value.to_string()[:-1]
|
||||
elif reply.type == xcffib.xproto.Atom.WINDOW:
|
||||
return int.from_bytes(reply.value.buf(), byteorder=sys.byteorder)
|
||||
elif reply.type in (xcffib.xproto.Atom.WINDOW, xcffib.xproto.Atom.CARDINAL):
|
||||
return struct.unpack("=I", reply.value.buf()[:4])[0]
|
||||
|
||||
return reply.value
|
||||
|
||||
@@ -150,101 +179,116 @@ class X11GameManager(GameManager):
|
||||
shm.detach()
|
||||
shm.remove()
|
||||
|
||||
@Slot(xcffib.Event)
|
||||
def on_game_opened(self, evt: xcffib.xproto.CreateNotifyEvent):
|
||||
self.logger.info("New game window opened %d", evt.window)
|
||||
self._instances[evt.window] = X11GameInstance(self, evt.window, parent=self)
|
||||
|
||||
@Slot(int)
|
||||
def on_game_closed(self, wid: int):
|
||||
if wid not in self._instances:
|
||||
return
|
||||
|
||||
self.logger.info("Game window %d closed", wid)
|
||||
instance = self._instances[wid]
|
||||
del self._instances[wid]
|
||||
self.instance_removed.emit(instance)
|
||||
self.instance_changed.emit()
|
||||
|
||||
|
||||
class X11EventWorker(QObject):
|
||||
on_active_window_changed = Signal(int)
|
||||
stop = False
|
||||
create_signal = Signal(xcffib.Event)
|
||||
destroy_signal = Signal(int)
|
||||
|
||||
def __init__(self, manager: "X11GameManager", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.manager = manager
|
||||
self.logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
|
||||
|
||||
self.handlers = ClassDict(
|
||||
{
|
||||
xcffib.xproto.PropertyNotifyEvent: self.on_property_change,
|
||||
xcffib.xinput.KeyPressEvent: self.on_key_press,
|
||||
xcffib.xinput.KeyReleaseEvent: self.on_key_press,
|
||||
}
|
||||
)
|
||||
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,
|
||||
}
|
||||
self.active_win_id = self.manager.get_active_window()
|
||||
|
||||
@Slot()
|
||||
def run(self):
|
||||
self.manager.connection.core.ChangeWindowAttributes(
|
||||
self.manager.connection.core.ChangeWindowAttributesChecked(
|
||||
self.manager.screen.root,
|
||||
xcffib.xproto.CW.EventMask,
|
||||
[xcffib.xproto.EventMask.PropertyChange],
|
||||
is_checked=True,
|
||||
)
|
||||
[
|
||||
xcffib.xproto.EventMask.PropertyChange
|
||||
| xcffib.xproto.EventMask.SubstructureNotify,
|
||||
],
|
||||
).check()
|
||||
current_thread = QThread.currentThread()
|
||||
|
||||
while True:
|
||||
if self.stop:
|
||||
self.logger.debug("Exiting")
|
||||
if current_thread.isInterruptionRequested():
|
||||
return
|
||||
|
||||
evt = self.manager.connection.wait_for_event()
|
||||
|
||||
if isinstance(evt, ge.GeGenericEvent):
|
||||
evt = evt.to_event(self.manager.connection)
|
||||
|
||||
try:
|
||||
handler = self.handlers[evt]
|
||||
except KeyError:
|
||||
self.logger.debug("Unhandled event %s", repr(evt))
|
||||
evt = self.manager.connection.poll_for_event()
|
||||
if evt is None:
|
||||
QThread.msleep(10)
|
||||
continue
|
||||
|
||||
try:
|
||||
handler(evt)
|
||||
except:
|
||||
self.logger.error("Error handling event %s", repr(evt), exc_info=True)
|
||||
|
||||
def on_key_press(self, evt: xcffib.xinput.KeyPressEvent):
|
||||
print(
|
||||
{
|
||||
"deviceid": evt.deviceid,
|
||||
"time": evt.time,
|
||||
"detail": evt.detail,
|
||||
"root": evt.root,
|
||||
"event": evt.event,
|
||||
"child": evt.child,
|
||||
"root_x": evt.root_x,
|
||||
"root_y": evt.root_y,
|
||||
"event_x": evt.event_x,
|
||||
"event_y": evt.event_y,
|
||||
"buttons_len": evt.buttons_len,
|
||||
"valuators_len": evt.valuators_len,
|
||||
"sourceid": evt.sourceid,
|
||||
"flags": evt.flags,
|
||||
}
|
||||
)
|
||||
try:
|
||||
instance = self.manager._instance[evt.root]
|
||||
except KeyError:
|
||||
self.logger.debug(
|
||||
"Unknown KeyPressEvent to instance %s: %s", evt.root, repr(evt)
|
||||
)
|
||||
self.manager.xinput.XIPassiveUngrabDevice(
|
||||
evt.root,
|
||||
evt.detail,
|
||||
evt.deviceid,
|
||||
1,
|
||||
xcffib.xinput.GrabType.Keycode,
|
||||
evt.mods.effective,
|
||||
)
|
||||
return
|
||||
|
||||
instance.key_press.emit(evt)
|
||||
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
|
||||
)
|
||||
|
||||
def on_property_change(self, evt: xcffib.xproto.PropertyNotifyEvent):
|
||||
if evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW):
|
||||
if (
|
||||
evt.state == xcffib.xproto.Property.NewValue
|
||||
and evt.atom == self.manager.get_atom(NET_ACTIVE_WINDOW)
|
||||
and evt.window == self.manager.screen.root
|
||||
):
|
||||
active_win_id = self.manager.get_active_window()
|
||||
|
||||
if self.active_win_id == active_win_id:
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def on_configure_event(self, evt: xcffib.xproto.ConfigureNotifyEvent):
|
||||
try:
|
||||
self.manager._instances[evt.window].config_signal.emit(evt)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def on_create(self, evt: xcffib.xproto.CreateNotifyEvent):
|
||||
if evt.window in self.manager._instances:
|
||||
return
|
||||
|
||||
# At this point we aren't sure if it's actually the game...
|
||||
if not self.manager.is_game(evt.window):
|
||||
return
|
||||
|
||||
self.create_signal.emit(evt)
|
||||
|
||||
def on_destroy(self, evt: xcffib.xproto.DestroyNotifyEvent):
|
||||
if evt.window in self.manager._instances:
|
||||
self.destroy_signal.emit(evt.window)
|
||||
@@ -1,7 +0,0 @@
|
||||
class ClassDict(dict[type]):
|
||||
def __getitem__(self, item):
|
||||
for key, value in self.items():
|
||||
if isinstance(item, key):
|
||||
return value
|
||||
|
||||
raise KeyError
|
||||
@@ -1,16 +1,13 @@
|
||||
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
|
||||
) -> Image:
|
||||
out = Image.frombytes(
|
||||
"RGBA",
|
||||
(width, height),
|
||||
shm.read(size),
|
||||
"raw",
|
||||
)
|
||||
b, g, r, a = out.split()
|
||||
a = ImageOps.invert(a)
|
||||
return Image.merge("RGBA", (r, g, b, a))
|
||||
) -> np.ndarray:
|
||||
out = np.frombuffer(shm.read(size), "<B").copy()
|
||||
out.shape = (height, width, 4)
|
||||
out[:, :, 3] = 0xFF # Convert BGRX to BGRA
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,194 @@
|
||||
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,29 +1,116 @@
|
||||
from typing import TYPE_CHECKING
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from PySide2.QtWidgets import QWidget
|
||||
from PySide2.QtCore import Slot, QCoreApplication
|
||||
from PySide2.QtWidgets import QMessageBox
|
||||
|
||||
from runekit.alt1.utils import fetch_manifest
|
||||
from runekit.app import App
|
||||
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.ui import AutoNotifier
|
||||
from runekit.ui.tray import TrayIcon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from runekit.game import GameInstance
|
||||
from runekit.game import GameInstance, GameManager
|
||||
|
||||
|
||||
class Host(QWidget):
|
||||
class Host:
|
||||
"""Host is the root of our application"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.notifier = AutoNotifier()
|
||||
open_app: List[App]
|
||||
|
||||
def start_app(self, manifest_url, instance: "GameInstance") -> App:
|
||||
manifest = fetch_manifest(manifest_url)
|
||||
def __init__(self, manager: "GameManager"):
|
||||
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]
|
||||
|
||||
app = App(
|
||||
host=self,
|
||||
app_id=appid,
|
||||
manifest=manifest,
|
||||
game_instance=instance,
|
||||
source_url=manifest_url,
|
||||
source_url=manifest["configUrl"],
|
||||
)
|
||||
app.get_window(parent=self).show()
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,221 @@
|
||||
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,27 +1 @@
|
||||
import logging
|
||||
import struct
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
from .format import image_to_bgra
|
||||
except ImportError:
|
||||
logging.warning("Cannot import native image code, using pure python")
|
||||
|
||||
image_8bpp = struct.Struct("BBBB")
|
||||
|
||||
def image_to_bgra(image: "Image", x, y, w, h) -> bytes:
|
||||
buf = bytearray(w * h * image_8bpp.size)
|
||||
index = 0
|
||||
for p in image.crop((x, y, x + w, y + h)).getdata():
|
||||
r = p[0]
|
||||
g = p[1]
|
||||
b = p[2]
|
||||
a = p[3] if len(p) == 4 else 255
|
||||
|
||||
image_8bpp.pack_into(buf, index, b, g, r, a)
|
||||
index += image_8bpp.size
|
||||
|
||||
return buf
|
||||
from .algo import is_color_percent_gte
|
||||
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -1,46 +0,0 @@
|
||||
from cpython.mem cimport PyMem_Malloc, PyMem_Free
|
||||
|
||||
cdef struct ImageRGBA:
|
||||
unsigned char r
|
||||
unsigned char g
|
||||
unsigned char b
|
||||
unsigned char a
|
||||
|
||||
def image_to_bgra(image, long ix, long iy, unsigned long w, unsigned long h):
|
||||
ptr = dict(image.im.unsafe_ptrs)['image32']
|
||||
cdef size_t ptr_int = <size_t> ptr
|
||||
cdef ImageRGBA **image32 = <ImageRGBA**> ptr_int
|
||||
|
||||
if ix < 0:
|
||||
ix = 0
|
||||
if iy < 0:
|
||||
iy = 0
|
||||
if ix + w > image.width:
|
||||
w = image.width - ix
|
||||
if iy + h > image.height:
|
||||
h = image.height - iy
|
||||
|
||||
cdef size_t size = w * h * 4
|
||||
cdef unsigned char *buf = <unsigned char*> PyMem_Malloc(size)
|
||||
|
||||
cdef size_t index, x, y
|
||||
cdef unsigned char r, g, b, a
|
||||
|
||||
with nogil:
|
||||
for y in range(iy, iy+h):
|
||||
for x in range(ix, ix+w):
|
||||
index = (((y-iy) * w) + (x-ix)) * 4
|
||||
r = image32[y][x].r
|
||||
g = image32[y][x].g
|
||||
b = image32[y][x].b
|
||||
a = image32[y][x].a
|
||||
|
||||
buf[index] = b
|
||||
buf[index+1] = g
|
||||
buf[index+2] = r
|
||||
buf[index+3] = a
|
||||
|
||||
try:
|
||||
return buf[:size]
|
||||
finally:
|
||||
PyMem_Free(buf)
|
||||
@@ -0,0 +1,63 @@
|
||||
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
|
||||
@@ -0,0 +1,82 @@
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import click
|
||||
from PySide2.QtCore import QSettings, Qt, QTimer
|
||||
from PySide2.QtWidgets import (
|
||||
QApplication,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
import runekit._resources
|
||||
from runekit import browser
|
||||
from runekit.game import get_platform_manager
|
||||
from runekit.host import Host
|
||||
|
||||
|
||||
@click.command(
|
||||
context_settings=dict(
|
||||
ignore_unknown_options=True,
|
||||
)
|
||||
)
|
||||
@click.option("--game-index", default=0, help="Game instance index, starting from 0")
|
||||
@click.argument("qt_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@click.argument("app_url", required=False)
|
||||
def main(app_url, game_index, qt_args):
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logging.info("Starting QtWebEngine")
|
||||
browser.init()
|
||||
|
||||
app = QApplication(["runekit", *qt_args])
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
app.setOrganizationName("cupco.de")
|
||||
app.setOrganizationDomain("cupco.de")
|
||||
app.setApplicationName("RuneKit")
|
||||
|
||||
signal.signal(signal.SIGINT, lambda no, frame: app.quit())
|
||||
|
||||
timer = QTimer()
|
||||
timer.start(300)
|
||||
timer.timeout.connect(lambda: None)
|
||||
|
||||
QSettings.setDefaultFormat(QSettings.IniFormat)
|
||||
|
||||
try:
|
||||
game_manager = get_platform_manager()
|
||||
host = Host(game_manager)
|
||||
|
||||
if app_url == "settings":
|
||||
host.open_settings()
|
||||
host.setting_dialog.setAttribute(Qt.WA_DeleteOnClose)
|
||||
host.setting_dialog.destroyed.connect(app.quit)
|
||||
elif app_url:
|
||||
logging.info("Loading app")
|
||||
game_app = host.launch_app_from_url(app_url)
|
||||
game_app.window.destroyed.connect(app.quit)
|
||||
else:
|
||||
if not host.app_store.has_default_apps():
|
||||
host.app_store.load_default_apps()
|
||||
|
||||
app.exec_()
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
msg = QMessageBox(
|
||||
QMessageBox.Critical,
|
||||
"Oh No!",
|
||||
f"Fatal error: \n\n{e.__class__.__name__}: {e}",
|
||||
)
|
||||
msg.setDetailedText(traceback.format_exc())
|
||||
msg.exec_()
|
||||
|
||||
raise
|
||||
finally:
|
||||
if game_manager is not None:
|
||||
logging.debug("Stopping game manager")
|
||||
game_manager.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import QRect
|
||||
from PySide2.QtWidgets import QMainWindow
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from runekit.app import App
|
||||
|
||||
|
||||
class GameSnapMixin(QMainWindow):
|
||||
app: "App"
|
||||
__last_game_pos: QRect
|
||||
|
||||
def snap_to_game(self):
|
||||
rect = self.app.game_instance.get_position()
|
||||
pos = rect.topLeft()
|
||||
if pos.x() < 0:
|
||||
pos.setX(0)
|
||||
if pos.y() < 0:
|
||||
pos.setY(0)
|
||||
|
||||
self.move(pos)
|
||||
self.__last_game_pos = self.app.game_instance.get_position()
|
||||
self.app.game_instance.positionChanged.connect(self._update_game_snap)
|
||||
|
||||
def _update_game_snap(self, game_pos: QRect):
|
||||
# If the top left spot of us fall into game window, then we move with the game
|
||||
tl = self.geometry().topLeft()
|
||||
if not game_pos.contains(tl):
|
||||
self.__last_game_pos = game_pos
|
||||
return
|
||||
|
||||
dx = game_pos.x() - self.__last_game_pos.x()
|
||||
dy = game_pos.y() - self.__last_game_pos.y()
|
||||
|
||||
pos = self.pos()
|
||||
pos.setX(pos.x() + dx)
|
||||
pos.setY(pos.y() + dy)
|
||||
self.move(pos)
|
||||
|
||||
self.__last_game_pos = game_pos
|
||||
@@ -1,13 +1,11 @@
|
||||
import abc
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
from typing import Union, Dict
|
||||
|
||||
from PySide2.QtGui import QIcon
|
||||
from PySide2.QtCore import QObject, QSettings
|
||||
from PySide2.QtWidgets import QSystemTrayIcon
|
||||
|
||||
from .tooltip import TooltipManager
|
||||
|
||||
TRAY_ICON = Path(__file__).parent / "trayicon.png"
|
||||
from .tray import tray_icon
|
||||
|
||||
|
||||
class Notifier(abc.ABC):
|
||||
@@ -16,6 +14,11 @@ class Notifier(abc.ABC):
|
||||
...
|
||||
|
||||
|
||||
class StubNotifier(Notifier):
|
||||
def notify(self, text):
|
||||
pass
|
||||
|
||||
|
||||
class TooltipNotifier(Notifier):
|
||||
def __init__(self):
|
||||
self.manager = TooltipManager()
|
||||
@@ -25,26 +28,46 @@ 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:
|
||||
self.icon.showMessage("RuneKit", text)
|
||||
tray_icon().showMessage("RuneKit", text)
|
||||
|
||||
|
||||
class AutoNotifier(Notifier):
|
||||
class AutoNotifier(QObject):
|
||||
notifier: Notifier
|
||||
|
||||
METHOD_STUB = 0
|
||||
METHOD_NOTIFICATION = 1
|
||||
METHOD_TOOLTIP = 2
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.settings = QSettings(self)
|
||||
type_ = int(self.settings.value("settings/tooltip", self.METHOD_NOTIFICATION))
|
||||
if (
|
||||
QSystemTrayIcon.isSystemTrayAvailable()
|
||||
and QSystemTrayIcon.supportsMessages()
|
||||
and type_ == self.METHOD_NOTIFICATION
|
||||
):
|
||||
self.notifier = TrayIconNotifier()
|
||||
elif type_ == self.METHOD_TOOLTIP:
|
||||
self.notifier = TooltipNotifier()
|
||||
else:
|
||||
self.notifier = StubNotifier()
|
||||
|
||||
def notify(self, text):
|
||||
self.notifier.notify(text)
|
||||
|
||||
@classmethod
|
||||
def availableMethods(cls) -> Dict[int, str]:
|
||||
out = {
|
||||
cls.METHOD_STUB: "Disabled",
|
||||
cls.METHOD_TOOLTIP: "Cursor tooltip",
|
||||
}
|
||||
if (
|
||||
QSystemTrayIcon.isSystemTrayAvailable()
|
||||
and QSystemTrayIcon.supportsMessages()
|
||||
):
|
||||
self.notifier = TrayIconNotifier()
|
||||
else:
|
||||
self.notifier = TooltipNotifier()
|
||||
out[cls.METHOD_NOTIFICATION] = "Notification"
|
||||
|
||||
def notify(self, text):
|
||||
self.notifier.notify(text)
|
||||
return out
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 941 B |
|
After Width: | Height: | Size: 241 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 182 B |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 266 B |
|
After Width: | Height: | Size: 264 B |
|
After Width: | Height: | Size: 248 B |
|
After Width: | Height: | Size: 189 B |
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"url": "https://runeapps.org/nis/default/",
|
||||
|
||||
"borderTop": 4,
|
||||
"borderLeft": 4,
|
||||
"borderRight": 4,
|
||||
"borderBottom": 4,
|
||||
|
||||
"addressHeight": 28,
|
||||
"titleHeight": 36,
|
||||
|
||||
"fontColor": "192,192,192",
|
||||
"titleColor": "192,192,192",
|
||||
"addressBackColor": "76,78,84"
|
||||
}
|
||||
|
After Width: | Height: | Size: 424 B |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 373 B |
|
After Width: | Height: | Size: 372 B |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 448 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,6 +1,6 @@
|
||||
from typing import Union, Optional
|
||||
from typing import Union
|
||||
|
||||
from PySide2.QtCore import Qt, QTimer, Slot, QSize
|
||||
from PySide2.QtCore import Qt, QTimer, Slot
|
||||
from PySide2.QtGui import QCursor
|
||||
from PySide2.QtWidgets import QLabel, QMainWindow
|
||||
|
||||
@@ -15,8 +15,8 @@ class TooltipManager(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
flags=Qt.Dialog
|
||||
| Qt.X11BypassWindowManagerHint
|
||||
flags=Qt.Popup
|
||||
| Qt.FramelessWindowHint
|
||||
| Qt.WindowStaysOnTopHint
|
||||
| Qt.WindowTransparentForInput
|
||||
)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide2.QtCore import Slot, Signal, QCoreApplication
|
||||
from PySide2.QtGui import QIcon
|
||||
from PySide2.QtWidgets import QSystemTrayIcon, QMenu
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from runekit.host import Host
|
||||
|
||||
|
||||
class TrayIcon(QSystemTrayIcon):
|
||||
on_settings = Signal()
|
||||
|
||||
host: "Host"
|
||||
|
||||
def __init__(self, host: "Host"):
|
||||
global _tray_icon
|
||||
super().__init__(QIcon(":/runekit/ui/trayicon.png"))
|
||||
|
||||
_tray_icon = self
|
||||
|
||||
self.host = host
|
||||
self._setup_menu()
|
||||
self.setContextMenu(self.menu)
|
||||
|
||||
def _setup_menu(self):
|
||||
if not hasattr(self, "menu"):
|
||||
self.menu = QMenu("RuneKit")
|
||||
|
||||
self.menu.clear()
|
||||
|
||||
self._setup_app_menu("", self.menu)
|
||||
|
||||
self.menu.addSeparator()
|
||||
self.menu_settings = self.menu.addAction("Settings")
|
||||
self.menu_settings.triggered.connect(self.on_settings)
|
||||
self.menu.addAction(
|
||||
QIcon.fromTheme("application-exit"),
|
||||
"Exit",
|
||||
lambda: QCoreApplication.instance().quit(),
|
||||
)
|
||||
|
||||
def _setup_app_menu(self, path: str, menu: QMenu):
|
||||
for app_id, manifest in self.host.app_store.list_app(path):
|
||||
if manifest is None:
|
||||
# Folder
|
||||
submenu = menu.addMenu(QIcon.fromTheme("folder"), app_id)
|
||||
self._setup_app_menu(app_id, submenu)
|
||||
continue
|
||||
|
||||
app_menu = menu.addAction(manifest["appName"])
|
||||
app_menu.triggered.connect(
|
||||
lambda _=None, app_id=app_id: self.host.launch_app_id(app_id)
|
||||
)
|
||||
app_menu.setToolTip(manifest["description"])
|
||||
|
||||
icon = self.host.app_store.icon(app_id)
|
||||
if icon:
|
||||
app_menu.setIcon(icon)
|
||||
|
||||
@Slot()
|
||||
def update_menu(self):
|
||||
self._setup_menu()
|
||||
|
||||
|
||||
def tray_icon() -> TrayIcon:
|
||||
return _tray_icon
|
||||
@@ -0,0 +1,241 @@
|
||||
from PySide2.QtCore import Signal, QPoint
|
||||
from PySide2.QtGui import QPixmap, Qt, QResizeEvent
|
||||
from PySide2.QtWidgets import (
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
SKIN = ":/runekit/ui/skins/default/"
|
||||
|
||||
|
||||
class WindowFrame(QWidget):
|
||||
on_minimize = Signal()
|
||||
on_settings = Signal()
|
||||
on_exit = Signal()
|
||||
|
||||
content: QWidget
|
||||
shade = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout()
|
||||
|
||||
def _layout(self):
|
||||
self._inner = _FrameInner(parent=self)
|
||||
self._buttons = _WindowButtons(parent=self)
|
||||
self._buttons.on_minimize.connect(self.on_minimize)
|
||||
self._buttons.on_minimize.connect(self.handle_shade)
|
||||
self._buttons.on_settings.connect(self.on_settings)
|
||||
self._buttons.on_exit.connect(self.on_exit)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setMargin(0)
|
||||
self.setLayout(layout)
|
||||
layout.addWidget(self._inner)
|
||||
self._buttons.show()
|
||||
|
||||
def resizeEvent(self, evt: QResizeEvent):
|
||||
buttons_size = self._buttons.sizeHint()
|
||||
self._buttons.setGeometry(
|
||||
evt.size().width() - buttons_size.width(),
|
||||
0,
|
||||
buttons_size.width(),
|
||||
buttons_size.height(),
|
||||
)
|
||||
|
||||
def set_content(self, content: QWidget):
|
||||
self._inner.set_content(content)
|
||||
self.content = content
|
||||
|
||||
def handle_shade(self):
|
||||
self.set_shade(not self.shade)
|
||||
|
||||
def set_shade(self, shade: bool):
|
||||
if shade == self.shade:
|
||||
return
|
||||
|
||||
self.shade = shade
|
||||
|
||||
if self.shade:
|
||||
self.previous_window_geom = self.window().size()
|
||||
self.previous_window_min_size = self.window().minimumSize()
|
||||
top_right = self.window().geometry().topRight()
|
||||
self._inner.hide()
|
||||
self.window().setMinimumSize(self._buttons.sizeHint())
|
||||
|
||||
size = self._buttons.sizeHint()
|
||||
self.window().resize(size)
|
||||
|
||||
new_top_left = QPoint(top_right)
|
||||
new_top_left.setX(top_right.x() - size.width())
|
||||
self.window().move(new_top_left)
|
||||
else:
|
||||
pos = self.window().geometry().topRight()
|
||||
self._inner.show()
|
||||
if hasattr(self, "previous_window_geom"):
|
||||
self.window().setMinimumSize(self.previous_window_min_size)
|
||||
geom = self.previous_window_geom
|
||||
else:
|
||||
geom = self.window().sizeHint()
|
||||
|
||||
pos.setX(pos.x() - geom.width())
|
||||
self.window().resize(geom)
|
||||
self.window().move(pos)
|
||||
|
||||
|
||||
class _FrameInner(QWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout()
|
||||
# TODO: Set background
|
||||
|
||||
def _layout(self):
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
top_row = QHBoxLayout()
|
||||
layout.addLayout(top_row)
|
||||
top_row.setContentsMargins(0, 0, 0, 0)
|
||||
top_row.setSpacing(0)
|
||||
top_row.addWidget(self.build_image("borderTL", Qt.TopEdge | Qt.LeftEdge))
|
||||
top_row.addWidget(self.build_image("borderT", Qt.TopEdge, h_fix=False))
|
||||
top_row.addWidget(self.build_image("borderTR", Qt.TopEdge | Qt.RightEdge))
|
||||
|
||||
mid_row = QHBoxLayout()
|
||||
layout.addLayout(mid_row)
|
||||
mid_row.setContentsMargins(0, 0, 0, 0)
|
||||
mid_row.setSpacing(0)
|
||||
mid_row.addWidget(self.build_image("borderL", Qt.LeftEdge, v_fix=False))
|
||||
self.content = QWidget(self)
|
||||
mid_row.addWidget(self.content, 1)
|
||||
mid_row.addWidget(self.build_image("borderR", Qt.RightEdge, v_fix=False))
|
||||
|
||||
bot_row = QHBoxLayout()
|
||||
layout.addLayout(bot_row)
|
||||
bot_row.setContentsMargins(0, 0, 0, 0)
|
||||
bot_row.setSpacing(0)
|
||||
bot_row.addWidget(
|
||||
self.build_image(
|
||||
"borderBL",
|
||||
Qt.BottomEdge | Qt.LeftEdge,
|
||||
)
|
||||
)
|
||||
bot_row.addWidget(self.build_image("borderB", Qt.BottomEdge, h_fix=False))
|
||||
bot_row.addWidget(self.build_image("borderBR", Qt.BottomEdge | Qt.RightEdge))
|
||||
|
||||
def build_image(self, name: str, edge=None, h_fix=True, v_fix=True):
|
||||
pixmap = QPixmap(SKIN + name)
|
||||
label = _ResizeHandle(edge, parent=self)
|
||||
label.setSizePolicy(
|
||||
QSizePolicy.Fixed if h_fix else QSizePolicy.Preferred,
|
||||
QSizePolicy.Fixed if v_fix else QSizePolicy.Preferred,
|
||||
)
|
||||
label.setScaledContents(True)
|
||||
label.setPixmap(pixmap)
|
||||
return label
|
||||
|
||||
def set_content(self, content: QWidget):
|
||||
self.layout().replaceWidget(self.content, content)
|
||||
self.content = content
|
||||
|
||||
|
||||
class _WindowButtons(QWidget):
|
||||
on_minimize = Signal()
|
||||
on_settings = Signal()
|
||||
on_exit = Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout()
|
||||
|
||||
def _layout(self):
|
||||
layout = QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addStretch(1)
|
||||
|
||||
drag_area = _DragHandle(self)
|
||||
layout.addWidget(drag_area, alignment=Qt.AlignTop)
|
||||
|
||||
minimize_button = QPushButton(self)
|
||||
minimize_button.clicked.connect(self.on_minimize)
|
||||
minimize_button.setStyleSheet(
|
||||
self.button_style("minimize.png", "minimizeHover.png")
|
||||
)
|
||||
layout.addWidget(minimize_button, alignment=Qt.AlignTop)
|
||||
|
||||
# settings_button = QPushButton(self)
|
||||
# settings_button.clicked.connect(self.on_settings)
|
||||
# settings_button.setStyleSheet(
|
||||
# self.button_style("settings.png", "settingsHover.png")
|
||||
# )
|
||||
# layout.addWidget(settings_button, alignment=Qt.AlignTop)
|
||||
|
||||
exit_button = QPushButton(self)
|
||||
exit_button.clicked.connect(self.on_exit)
|
||||
exit_button.setStyleSheet(self.button_style("exit.png", "exitHover.png"))
|
||||
layout.addWidget(exit_button, alignment=Qt.AlignTop)
|
||||
|
||||
def button_style(self, normal, hover):
|
||||
return f"""
|
||||
QPushButton {{
|
||||
image: url({SKIN}{normal});
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}}
|
||||
QPushButton::hover {{
|
||||
image: url({SKIN}{hover});
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
class _DragHandle(QLabel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.setPixmap(QPixmap(SKIN + "borderDrag.png"))
|
||||
self.setCursor(Qt.OpenHandCursor)
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
self.window().windowHandle().startSystemMove()
|
||||
|
||||
|
||||
class _ResizeHandle(QLabel):
|
||||
def __init__(self, edge, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.edge = edge
|
||||
|
||||
if edge == Qt.TopEdge & Qt.LeftEdge:
|
||||
self.setCursor(Qt.SizeFDiagCursor)
|
||||
elif edge == Qt.TopEdge:
|
||||
self.setCursor(Qt.SizeVerCursor)
|
||||
elif edge == Qt.TopEdge & Qt.RightEdge:
|
||||
self.setCursor(Qt.SizeBDiagCursor)
|
||||
elif edge == Qt.LeftEdge:
|
||||
self.setCursor(Qt.SizeHorCursor)
|
||||
elif edge == Qt.RightEdge:
|
||||
self.setCursor(Qt.SizeHorCursor)
|
||||
elif edge == Qt.BottomEdge & Qt.LeftEdge:
|
||||
self.setCursor(Qt.SizeBDiagCursor)
|
||||
elif edge == Qt.BottomEdge:
|
||||
self.setCursor(Qt.SizeVerCursor)
|
||||
elif edge == Qt.BottomEdge & Qt.RightEdge:
|
||||
self.setCursor(Qt.SizeFDiagCursor)
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
if self.edge:
|
||||
self.window().windowHandle().startSystemResize(self.edge)
|
||||