mirror of
https://github.com/csd4ni3l/browser.git
synced 2026-04-17 16:06:03 +02:00
Compare commits
9 Commits
latest
...
56f613f531
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56f613f531 | ||
|
|
6c0b5180b4 | ||
|
|
82ecd8cf8f | ||
|
|
0b2e9c12ad | ||
|
|
fc4fc66b30 | ||
|
|
b7dedd7273 | ||
|
|
f46682bfdf | ||
|
|
dc1cf67527 | ||
|
|
c67cfcc1cb |
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[target.'cfg(target_os = "linux")']
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]
|
||||||
115
.github/workflows/main.yml
vendored
115
.github/workflows/main.yml
vendored
@@ -8,85 +8,88 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
platform: linux
|
||||||
|
- os: windows-latest
|
||||||
|
platform: windows
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check-out repository
|
- name: Check-out repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Cache
|
||||||
uses: actions/setup-python@v5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
path: |
|
||||||
architecture: "x64"
|
~/.cargo/registry
|
||||||
cache: "pip"
|
~/.cargo/git
|
||||||
cache-dependency-path: |
|
target
|
||||||
**/requirements*.txt
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install mold, clang, Wayland, ALSA and x11 headers and dependencies (Linux)
|
||||||
run: pip install -r requirements.txt
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
|
||||||
- name: Build Executable
|
|
||||||
uses: Nuitka/Nuitka-Action@main
|
|
||||||
with:
|
|
||||||
nuitka-version: main
|
|
||||||
script-name: run.py
|
|
||||||
nofollow-import-to: "*tk*,_codecs,encodings,multiprocessing,gi"
|
|
||||||
disable-plugins: tk-inter,dill-compat,eventlet,gevent,pyqt5,pyqt6,pyside2,pyside6,delvewheel,pywebview,matplotlib,spacy,enum-compat,pbr-compat,gevent,pmw-freezer,transformers,upx,kivy,options-nanny,multiprocessing,gi
|
|
||||||
include-data-dir: assets=assets
|
|
||||||
include-data-files: CREDITS=CREDITS
|
|
||||||
mode: standalone
|
|
||||||
output-file: csd4ni3lBrowser
|
|
||||||
|
|
||||||
- name: Zip Build Output
|
|
||||||
shell: bash
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p zip_output
|
sudo apt update
|
||||||
if [ "${{ runner.os }}" = "Windows" ]; then
|
sudo apt install -y build-essential clang cmake pkg-config mold \
|
||||||
powershell.exe -Command "Compress-Archive -Path 'build/run.dist/*' -DestinationPath 'zip_output/csd4ni3lBrowser-${{ runner.os }}.zip'"
|
libwayland-dev libxkbcommon-dev libegl1-mesa-dev \
|
||||||
else
|
libwayland-egl-backend-dev \
|
||||||
cd build/run.dist
|
libx11-dev libxext-dev libxrandr-dev libxinerama-dev libxcursor-dev \
|
||||||
zip -r "../../zip_output/csd4ni3lBrowser-${{ runner.os }}.zip" .
|
libxi-dev libxfixes-dev libxrender-dev \
|
||||||
fi
|
libfreetype6-dev libfontconfig1-dev libgl1-mesa-dev \
|
||||||
|
libasound2-dev libudev-dev
|
||||||
|
|
||||||
- name: Upload Zipped Build Artifact
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --verbose
|
||||||
|
|
||||||
|
- name: Verify executable (Linux)
|
||||||
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
run: test target/release/csd4ni3l-browser
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Verify executable (Windows)
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: Test-Path target\release\csd4ni3l-browser.exe
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: csd4ni3lBrowser-${{ runner.os }}.zip
|
name: ${{ matrix.platform }}
|
||||||
path: zip_output/csd4ni3lBrowser-${{ runner.os }}.zip
|
path: |
|
||||||
|
target/release/csd4ni3l-browser
|
||||||
|
target/release/csd4ni3l-browser.exe
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create GitHub Release
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download All Zipped Builds
|
- name: Download All Build Artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: downloads
|
path: downloads
|
||||||
|
|
||||||
- name: Delete Old Release (if exists)
|
- name: Create release (if missing) and upload artifacts to tag
|
||||||
continue-on-error: true
|
|
||||||
run: gh release delete latest -y
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Delete Git tag (if exists)
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
git push origin :refs/tags/latest
|
set -euo pipefail
|
||||||
git tag -d latest
|
TAG="latest"
|
||||||
|
echo "Target release tag: $TAG"
|
||||||
- name: Recreate Git tag at HEAD
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||||
run: |
|
echo "Release $TAG already exists; will upload assets with --clobber"
|
||||||
git tag latest
|
else
|
||||||
git push origin latest
|
gh release create "$TAG" \
|
||||||
|
--title "$TAG" \
|
||||||
- name: Create the new release
|
--notes "Automated build for $TAG"
|
||||||
run: gh release create latest downloads/**/csd4ni3lBrowser-*.zip --title "Latest Build" --notes "Most recent multi-platform builds of csd4ni3lBrowser"
|
fi
|
||||||
env:
|
# Upload the executables directly (no zip files)
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
gh release upload "$TAG" downloads/linux/csd4ni3l-browser --clobber
|
||||||
|
gh release upload "$TAG" downloads/windows/csd4ni3l-browser.exe --clobber
|
||||||
188
.gitignore
vendored
188
.gitignore
vendored
@@ -1,184 +1,4 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
python
|
||||||
__pycache__/
|
/target
|
||||||
*.py[cod]
|
css_cache
|
||||||
*$py.class
|
html_cache
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# UV
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
#uv.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
# Ruff stuff:
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# PyPI configuration file
|
|
||||||
.pypirc
|
|
||||||
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
test*.py
|
|
||||||
.zed/
|
|
||||||
logs/
|
|
||||||
logs
|
|
||||||
settings.json
|
|
||||||
html_cache/
|
|
||||||
css_cache/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.11
|
|
||||||
11
CREDITS
11
CREDITS
@@ -4,12 +4,7 @@ The Roboto Black font used in this project is licensed under the Open Font Licen
|
|||||||
|
|
||||||
Thanks to https://browser.engineering for their tutorial on how to make a web browser from scratch!
|
Thanks to https://browser.engineering for their tutorial on how to make a web browser from scratch!
|
||||||
|
|
||||||
Huge Thanks to Python for being the programming language used in this game.
|
Huge Thanks to Rust for being the programming language used in this app.
|
||||||
https://www.python.org/
|
https://www.rust-lang.org/
|
||||||
|
|
||||||
Huge thanks to Arcade and Pyglet for being the graphical engines used in this application.
|
Huge thanks to Bevy for being the graphical engine / ECS used in this app.
|
||||||
https://arcade.academy/
|
|
||||||
https://pyglet.readthedocs.io/en/latest/
|
|
||||||
|
|
||||||
Thanks to the following other libraries used in this game:
|
|
||||||
pypresence - https://github.com/qwertyquerty/pypresence - Used for Discord Rich Presence
|
|
||||||
6017
Cargo.lock
generated
Normal file
6017
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
Cargo.toml
Normal file
53
Cargo.toml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
[package]
|
||||||
|
name = "csd4ni3l-browser"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64 = "0.22.1"
|
||||||
|
bevy_egui = "0.38.1"
|
||||||
|
native-tls = "0.2.18"
|
||||||
|
rand = "0.9.2"
|
||||||
|
rfd = "0.16.0"
|
||||||
|
serde = "1.0.228"
|
||||||
|
serde_json = "1.0.146"
|
||||||
|
|
||||||
|
[dependencies.bevy]
|
||||||
|
version = "0.17.3"
|
||||||
|
default-features = false
|
||||||
|
features = [
|
||||||
|
"bevy_log",
|
||||||
|
"bevy_window",
|
||||||
|
"bevy_winit",
|
||||||
|
"bevy_render",
|
||||||
|
"bevy_core_pipeline",
|
||||||
|
"bevy_sprite",
|
||||||
|
"bevy_text",
|
||||||
|
"bevy_ui",
|
||||||
|
"bevy_asset",
|
||||||
|
"bevy_picking",
|
||||||
|
"multi_threaded",
|
||||||
|
]
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 2
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies.bevy]
|
||||||
|
default-features = false
|
||||||
|
version = "0.17.3"
|
||||||
|
features = [
|
||||||
|
"bevy_log",
|
||||||
|
"bevy_window",
|
||||||
|
"bevy_winit",
|
||||||
|
"bevy_render",
|
||||||
|
"bevy_core_pipeline",
|
||||||
|
"bevy_sprite",
|
||||||
|
"bevy_text",
|
||||||
|
"bevy_ui",
|
||||||
|
"multi_threaded",
|
||||||
|
"bevy_asset",
|
||||||
|
"bevy_picking",
|
||||||
|
"wayland",
|
||||||
|
"x11"
|
||||||
|
]
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
import socket, logging, ssl, threading, os, ujson, time
|
|
||||||
|
|
||||||
from http_client.html_parser import HTML, CSSParser, Element, tree_to_list, get_inline_styles
|
|
||||||
|
|
||||||
def resolve_url(scheme, host, port, path, url):
|
|
||||||
if "://" in url: return url
|
|
||||||
if not url.startswith("/"):
|
|
||||||
dir, _ = path.rsplit("/", 1)
|
|
||||||
while url.startswith("../"):
|
|
||||||
_, url = url.split("/", 1)
|
|
||||||
if "/" in dir:
|
|
||||||
dir, _ = dir.rsplit("/", 1)
|
|
||||||
url = f"{dir}/{url}"
|
|
||||||
|
|
||||||
if url.startswith("//"):
|
|
||||||
return f"{scheme}:{url}"
|
|
||||||
else:
|
|
||||||
return f"{scheme}://{host}:{port}{url}"
|
|
||||||
|
|
||||||
class HTTPClient():
|
|
||||||
def __init__(self):
|
|
||||||
self.scheme = "http"
|
|
||||||
self.host = ""
|
|
||||||
self.path = ""
|
|
||||||
self.port = 0
|
|
||||||
self.request_headers = {}
|
|
||||||
self.response_explanation = None
|
|
||||||
self.response_headers = {}
|
|
||||||
self.response_http_version = None
|
|
||||||
self.response_status = None
|
|
||||||
self.nodes = None
|
|
||||||
self.css_rules = []
|
|
||||||
self.content_response = ""
|
|
||||||
self.view_source = False
|
|
||||||
self.redirect_count = 0
|
|
||||||
self.needs_render = False
|
|
||||||
|
|
||||||
def file_request(self, url):
|
|
||||||
with open(url.split("file://", 1)[1], "r") as file:
|
|
||||||
self.content_response = file.read()
|
|
||||||
|
|
||||||
def get_request(self, url, request_headers, css=False):
|
|
||||||
if url.startswith("view-source:"):
|
|
||||||
url = url.split("view-source:")[1]
|
|
||||||
self.view_source = True
|
|
||||||
else:
|
|
||||||
self.view_source = False
|
|
||||||
|
|
||||||
self.scheme, url_parts = url.split("://", 1)
|
|
||||||
|
|
||||||
if "/" not in url_parts:
|
|
||||||
self.host = url_parts
|
|
||||||
self.path = "/"
|
|
||||||
else:
|
|
||||||
self.host, self.path = url_parts.split("/", 1)
|
|
||||||
self.path = f"/{self.path}"
|
|
||||||
|
|
||||||
if ":" in self.host:
|
|
||||||
self.host, port = self.host.split(":", 1)
|
|
||||||
self.port = int(port)
|
|
||||||
else:
|
|
||||||
self.port = 80 if self.scheme == "http" else 443
|
|
||||||
|
|
||||||
self.request_headers = request_headers
|
|
||||||
self.response_explanation = None
|
|
||||||
self.response_headers = {}
|
|
||||||
self.response_http_version = None
|
|
||||||
self.response_status = None
|
|
||||||
self.content_response = ""
|
|
||||||
|
|
||||||
if "Host" not in self.request_headers:
|
|
||||||
self.request_headers["Host"] = self.host
|
|
||||||
|
|
||||||
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.html"
|
|
||||||
if os.path.exists(f"html_cache/{cache_filename}"):
|
|
||||||
threading.Thread(target=self.parse, daemon=True).start()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
self.socket.connect((self.host, self.port))
|
|
||||||
|
|
||||||
if self.scheme == "https":
|
|
||||||
ctx = ssl.create_default_context()
|
|
||||||
try:
|
|
||||||
self.socket = ctx.wrap_socket(self.socket, server_hostname=self.host)
|
|
||||||
except ssl.SSLCertVerificationError:
|
|
||||||
logging.debug(f"Invalid SSL cert for {self.host}:{self.port}{self.path}")
|
|
||||||
return
|
|
||||||
|
|
||||||
request_header_lines = '\r\n'.join([f"{header_name}: {header_value}" for header_name, header_value in self.request_headers.items()])
|
|
||||||
request = f"GET {self.path} HTTP/1.0\r\n{request_header_lines}\r\n\r\n"
|
|
||||||
|
|
||||||
logging.debug(f"Sending Request:\n{request}")
|
|
||||||
|
|
||||||
self.socket.send(request.encode())
|
|
||||||
|
|
||||||
threading.Thread(target=self.receive_response, daemon=True, args=(css,)).start()
|
|
||||||
|
|
||||||
def receive_response(self, css=False):
|
|
||||||
buffer = b""
|
|
||||||
headers_parsed = False
|
|
||||||
content_length = None
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
data = self.socket.recv(2048)
|
|
||||||
if not data:
|
|
||||||
logging.debug("Connection closed by peer.")
|
|
||||||
break
|
|
||||||
buffer += data
|
|
||||||
|
|
||||||
if not headers_parsed:
|
|
||||||
header_end_index = buffer.find(b"\r\n\r\n")
|
|
||||||
if header_end_index != -1: # not found
|
|
||||||
header_data = buffer[:header_end_index].decode('latin-1')
|
|
||||||
body_data = buffer[header_end_index + 4:] # +4 for the \r\n\r\n
|
|
||||||
|
|
||||||
self._parse_headers(header_data)
|
|
||||||
headers_parsed = True
|
|
||||||
|
|
||||||
content_length_header = self.response_headers.get("Content-Length")
|
|
||||||
if content_length_header:
|
|
||||||
try:
|
|
||||||
content_length = int(content_length_header)
|
|
||||||
except ValueError:
|
|
||||||
logging.debug(f"Invalid Content-Length header: {content_length_header}")
|
|
||||||
|
|
||||||
self.content_response = body_data.decode('utf-8', errors='ignore') # Assuming body is UTF-8
|
|
||||||
|
|
||||||
if content_length is not None and len(body_data) >= content_length:
|
|
||||||
break
|
|
||||||
elif content_length is None:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.content_response += data.decode('utf-8', errors='ignore')
|
|
||||||
if content_length is not None and len(self.content_response.encode('utf-8')) >= content_length:
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error receiving messages: {e}")
|
|
||||||
break
|
|
||||||
|
|
||||||
self.socket.close()
|
|
||||||
self.socket = None
|
|
||||||
|
|
||||||
if 300 <= int(self.response_status) < 400:
|
|
||||||
if self.redirect_count >= 4:
|
|
||||||
return
|
|
||||||
|
|
||||||
location_header = self.response_headers["Location"]
|
|
||||||
if "http" in location_header or "https" in location_header:
|
|
||||||
self.get_request(location_header, self.request_headers)
|
|
||||||
else:
|
|
||||||
self.get_request(f"{self.scheme}://{self.host}{location_header}", self.request_headers)
|
|
||||||
else:
|
|
||||||
self.redirect_count = 0
|
|
||||||
|
|
||||||
if not css:
|
|
||||||
self.parse()
|
|
||||||
|
|
||||||
def _parse_headers(self, header_data):
|
|
||||||
lines = header_data.splitlines()
|
|
||||||
|
|
||||||
if not lines:
|
|
||||||
logging.debug("Received empty header data.")
|
|
||||||
return
|
|
||||||
|
|
||||||
response_status_line = lines[0]
|
|
||||||
try:
|
|
||||||
self.response_http_version, self.response_status, *explanation_parts = response_status_line.split(" ", 2)
|
|
||||||
self.response_explanation = " ".join(explanation_parts)
|
|
||||||
except ValueError:
|
|
||||||
logging.error(f"Error parsing status line: {response_status_line}")
|
|
||||||
return
|
|
||||||
|
|
||||||
headers = {}
|
|
||||||
for i in range(1, len(lines)):
|
|
||||||
line = lines[i]
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
header_name, value = line.split(":", 1)
|
|
||||||
headers[header_name.strip()] = value.strip()
|
|
||||||
except ValueError:
|
|
||||||
logging.error(f"Error parsing header line: {line}")
|
|
||||||
self.response_headers = headers
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
self.css_rules = []
|
|
||||||
|
|
||||||
html_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.html"
|
|
||||||
|
|
||||||
if html_cache_filename in os.listdir("html_cache"):
|
|
||||||
with open(f"html_cache/{html_cache_filename}", "r") as file:
|
|
||||||
self.content_response = file.read()
|
|
||||||
else:
|
|
||||||
with open(f"html_cache/{html_cache_filename}", "w") as file:
|
|
||||||
file.write(self.content_response)
|
|
||||||
|
|
||||||
original_scheme = self.scheme
|
|
||||||
original_host = self.host
|
|
||||||
original_port = self.port
|
|
||||||
original_path = self.path
|
|
||||||
original_response = self.content_response
|
|
||||||
|
|
||||||
self.nodes = HTML(self.content_response).parse()
|
|
||||||
|
|
||||||
css_links = [
|
|
||||||
node.attributes["href"]
|
|
||||||
for node in tree_to_list(self.nodes, [])
|
|
||||||
if isinstance(node, Element)
|
|
||||||
and node.tag == "link"
|
|
||||||
and node.attributes.get("rel") == "stylesheet"
|
|
||||||
and "href" in node.attributes
|
|
||||||
]
|
|
||||||
|
|
||||||
for css_link in css_links:
|
|
||||||
self.content_response = ""
|
|
||||||
|
|
||||||
css_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path}_{css_link}.json".replace('/', '_').replace('@', '_').replace('/', '_').replace(';', '_').replace('&', '_').replace('?', '_').replace(':', '') # we need to include the other variables so for example /styles.css wouldnt be cached for all websites
|
|
||||||
|
|
||||||
if css_cache_filename in os.listdir("css_cache"):
|
|
||||||
with open(f"css_cache/{css_cache_filename}", "r") as file:
|
|
||||||
rules = CSSParser.from_json(ujson.load(file))
|
|
||||||
else:
|
|
||||||
self.get_request(resolve_url(self.scheme, self.host, self.port, self.path, css_link), self.request_headers, css=True)
|
|
||||||
while not self.content_response:
|
|
||||||
time.sleep(0.025)
|
|
||||||
|
|
||||||
rules = CSSParser(self.content_response).parse()
|
|
||||||
|
|
||||||
with open(f"css_cache/{css_cache_filename}", "w") as file:
|
|
||||||
ujson.dump(CSSParser.to_json(rules), file)
|
|
||||||
|
|
||||||
self.css_rules.extend(rules)
|
|
||||||
|
|
||||||
self.css_rules.extend(get_inline_styles(self.nodes))
|
|
||||||
|
|
||||||
self.scheme = original_scheme
|
|
||||||
self.host = original_host
|
|
||||||
self.port = original_port
|
|
||||||
self.path = original_path
|
|
||||||
self.content_response = original_response
|
|
||||||
self.needs_render = True
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
from utils.constants import SELF_CLOSING_TAGS, HEAD_TAGS, INHERITED_PROPERTIES
|
|
||||||
import html.entities
|
|
||||||
class Element:
|
|
||||||
def __init__(self, tag, attributes, parent):
|
|
||||||
self.tag = tag
|
|
||||||
self.attributes = attributes
|
|
||||||
self.children = []
|
|
||||||
self.parent = parent
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
attrs = [" " + k + "=\"" + v + "\"" for k, v in self.attributes.items()]
|
|
||||||
attr_str = ""
|
|
||||||
for attr in attrs:
|
|
||||||
attr_str += attr
|
|
||||||
return "<" + self.tag + attr_str + ">"
|
|
||||||
|
|
||||||
class Text:
|
|
||||||
def __init__(self, text, parent):
|
|
||||||
self.text = text
|
|
||||||
self.children = []
|
|
||||||
self.parent = parent
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return repr(self.text)
|
|
||||||
|
|
||||||
class HTML():
|
|
||||||
def __init__(self, raw_html):
|
|
||||||
self.raw_html = raw_html
|
|
||||||
self.unfinished = []
|
|
||||||
|
|
||||||
self.parse()
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
text = ""
|
|
||||||
in_tag = False
|
|
||||||
for c in self.raw_html:
|
|
||||||
if c == "<":
|
|
||||||
in_tag = True
|
|
||||||
if (not self.unfinished or not self.unfinished[-1].tag == "style") and text:
|
|
||||||
self.add_text(text) # start of new tag means before everything was content/text
|
|
||||||
text = ""
|
|
||||||
elif c == ">":
|
|
||||||
in_tag = False
|
|
||||||
self.add_tag(text) # end of a tag means everything in-between were tags
|
|
||||||
text = ""
|
|
||||||
else:
|
|
||||||
text += c
|
|
||||||
|
|
||||||
if not in_tag and text:
|
|
||||||
self.add_text(text)
|
|
||||||
|
|
||||||
return self.finish()
|
|
||||||
|
|
||||||
def add_text(self, text):
|
|
||||||
if text.isspace(): return
|
|
||||||
self.implicit_tags(None)
|
|
||||||
parent = self.unfinished[-1]
|
|
||||||
node = Text(text, parent)
|
|
||||||
parent.children.append(node)
|
|
||||||
|
|
||||||
def get_attributes(self, text):
|
|
||||||
parts = text.split()
|
|
||||||
tag = parts[0].casefold()
|
|
||||||
attributes = {}
|
|
||||||
|
|
||||||
for attrpair in parts[1:]:
|
|
||||||
if "=" in attrpair:
|
|
||||||
key, value = attrpair.split("=", 1)
|
|
||||||
if len(value) > 2 and value[0] in ["'", "\""]:
|
|
||||||
value = value[1:-1]
|
|
||||||
attributes[key.casefold()] = value
|
|
||||||
else:
|
|
||||||
attributes[attrpair.casefold()] = ""
|
|
||||||
|
|
||||||
return tag, attributes
|
|
||||||
|
|
||||||
def add_tag(self, tag):
|
|
||||||
tag, attributes = self.get_attributes(tag)
|
|
||||||
|
|
||||||
if tag.startswith("!"): return
|
|
||||||
|
|
||||||
self.implicit_tags(tag)
|
|
||||||
|
|
||||||
if tag.startswith("/"):
|
|
||||||
if len(self.unfinished) == 1: return
|
|
||||||
node = self.unfinished.pop()
|
|
||||||
parent = self.unfinished[-1]
|
|
||||||
parent.children.append(node)
|
|
||||||
elif tag in SELF_CLOSING_TAGS:
|
|
||||||
parent = self.unfinished[-1]
|
|
||||||
node = Element(tag, attributes, parent)
|
|
||||||
parent.children.append(node)
|
|
||||||
else:
|
|
||||||
parent = self.unfinished[-1] if self.unfinished else None
|
|
||||||
node = Element(tag, attributes, parent)
|
|
||||||
self.unfinished.append(node)
|
|
||||||
|
|
||||||
def implicit_tags(self, tag):
|
|
||||||
while True:
|
|
||||||
open_tags = [node.tag for node in self.unfinished]
|
|
||||||
if open_tags == [] and tag != "html":
|
|
||||||
self.add_tag("html")
|
|
||||||
elif open_tags == ["html"] and tag not in ["head", "body", "/html"]:
|
|
||||||
if tag in HEAD_TAGS:
|
|
||||||
self.add_tag("head")
|
|
||||||
else:
|
|
||||||
self.add_tag("body")
|
|
||||||
elif open_tags == ["html", "head"] and tag not in ["/head"] + HEAD_TAGS:
|
|
||||||
self.add_tag("/head")
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
def finish(self):
|
|
||||||
if not self.unfinished:
|
|
||||||
self.implicit_tags(None)
|
|
||||||
|
|
||||||
while len(self.unfinished) > 1:
|
|
||||||
node = self.unfinished.pop()
|
|
||||||
parent = self.unfinished[-1]
|
|
||||||
parent.children.append(node)
|
|
||||||
return self.unfinished.pop()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def print_tree(node, indent=0):
|
|
||||||
print(" " * indent, node)
|
|
||||||
for child in node.children:
|
|
||||||
HTML.print_tree(child, indent + 2)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def to_json(tree: Element | Text):
|
|
||||||
if isinstance(tree, Text):
|
|
||||||
return ["text", tree.text, [HTML.to_json(child) for child in tree.children]]
|
|
||||||
elif isinstance(tree, Element):
|
|
||||||
return ["element", tree.tag, tree.attributes, [HTML.to_json(child) for child in tree.children]]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_json(json_list, parent=None):
|
|
||||||
if json_list[0] == "text":
|
|
||||||
text = Text(json_list[1], parent)
|
|
||||||
text.children = [HTML.from_json(child, text) for child in json_list[2]]
|
|
||||||
return text
|
|
||||||
elif json_list[0] == "element":
|
|
||||||
element = Element(json_list[1], json_list[2], parent)
|
|
||||||
element.children = [HTML.from_json(child, element) for child in json_list[3]]
|
|
||||||
return element
|
|
||||||
|
|
||||||
class TagSelector:
|
|
||||||
def __init__(self, tag):
|
|
||||||
self.tag = tag
|
|
||||||
self.priority = 1
|
|
||||||
|
|
||||||
def matches(self, node):
|
|
||||||
return isinstance(node, Element) and self.tag == node.tag
|
|
||||||
|
|
||||||
class DescendantSelector:
|
|
||||||
def __init__(self, ancestor, descendant):
|
|
||||||
self.ancestor = ancestor
|
|
||||||
self.descendant = descendant
|
|
||||||
self.priority = ancestor.priority + descendant.priority
|
|
||||||
|
|
||||||
def matches(self, node):
|
|
||||||
if not self.descendant.matches(node): return False
|
|
||||||
while node.parent:
|
|
||||||
if self.ancestor.matches(node.parent): return True
|
|
||||||
node = node.parent
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cascade_priority(rule):
|
|
||||||
selector, body = rule
|
|
||||||
return selector.priority
|
|
||||||
|
|
||||||
def get_inline_styles(node):
|
|
||||||
all_rules = []
|
|
||||||
|
|
||||||
for node in node.children:
|
|
||||||
if isinstance(node, Element) and node.tag == "style":
|
|
||||||
if not node.children:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(node.children[0], Text):
|
|
||||||
all_rules.extend(CSSParser(node.children[0].text).parse()) # node's first children will just be a text element that contains the css
|
|
||||||
|
|
||||||
all_rules.extend(get_inline_styles(node))
|
|
||||||
|
|
||||||
return all_rules
|
|
||||||
|
|
||||||
class CSSParser:
|
|
||||||
def __init__(self, s):
|
|
||||||
self.s = s
|
|
||||||
self.i = 0
|
|
||||||
|
|
||||||
def whitespace(self):
|
|
||||||
while self.i < len(self.s) and self.s[self.i].isspace():
|
|
||||||
self.i += 1
|
|
||||||
|
|
||||||
def literal(self, literal):
|
|
||||||
if not (self.i < len(self.s) and self.s[self.i] == literal):
|
|
||||||
raise Exception("Parsing error")
|
|
||||||
self.i += 1
|
|
||||||
|
|
||||||
def word(self):
|
|
||||||
start = self.i
|
|
||||||
while self.i < len(self.s):
|
|
||||||
if self.s[self.i].isalnum() or self.s[self.i] in "#-.%":
|
|
||||||
self.i += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
if not (self.i > start):
|
|
||||||
raise Exception("Parsing error")
|
|
||||||
return self.s[start:self.i]
|
|
||||||
|
|
||||||
def pair(self):
|
|
||||||
prop = self.word()
|
|
||||||
|
|
||||||
self.whitespace()
|
|
||||||
self.literal(":")
|
|
||||||
self.whitespace()
|
|
||||||
|
|
||||||
val = self.word()
|
|
||||||
|
|
||||||
return prop.casefold(), val
|
|
||||||
|
|
||||||
def ignore_until(self, chars):
|
|
||||||
while self.i < len(self.s):
|
|
||||||
if self.s[self.i] in chars:
|
|
||||||
return self.s[self.i]
|
|
||||||
else:
|
|
||||||
self.i += 1
|
|
||||||
return None
|
|
||||||
|
|
||||||
def body(self):
|
|
||||||
pairs = {}
|
|
||||||
while self.i < len(self.s) and self.s[self.i] != "}":
|
|
||||||
try:
|
|
||||||
prop, val = self.pair()
|
|
||||||
pairs[prop] = val
|
|
||||||
|
|
||||||
self.whitespace()
|
|
||||||
|
|
||||||
self.literal(";")
|
|
||||||
|
|
||||||
self.whitespace()
|
|
||||||
except Exception:
|
|
||||||
why = self.ignore_until([";", "}"])
|
|
||||||
if why == ";":
|
|
||||||
self.literal(";")
|
|
||||||
self.whitespace()
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
return pairs
|
|
||||||
|
|
||||||
def selector(self):
|
|
||||||
out = TagSelector(self.word().casefold())
|
|
||||||
self.whitespace()
|
|
||||||
while self.i < len(self.s) and self.s[self.i] != "{":
|
|
||||||
tag = self.word()
|
|
||||||
descendant = TagSelector(tag.casefold())
|
|
||||||
out = DescendantSelector(out, descendant)
|
|
||||||
self.whitespace()
|
|
||||||
return out
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
rules = []
|
|
||||||
while self.i < len(self.s):
|
|
||||||
try:
|
|
||||||
self.whitespace()
|
|
||||||
|
|
||||||
selector = self.selector()
|
|
||||||
|
|
||||||
self.literal("{")
|
|
||||||
|
|
||||||
self.whitespace()
|
|
||||||
|
|
||||||
body = self.body()
|
|
||||||
|
|
||||||
self.literal("}")
|
|
||||||
|
|
||||||
rules.append((selector, body))
|
|
||||||
except Exception:
|
|
||||||
why = self.ignore_until(["}"])
|
|
||||||
if why == "}":
|
|
||||||
self.literal("}")
|
|
||||||
self.whitespace()
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return rules
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_selector_to_json(self, selector):
|
|
||||||
if isinstance(selector, TagSelector):
|
|
||||||
return ["tag", selector.tag, selector.priority]
|
|
||||||
elif isinstance(selector, DescendantSelector):
|
|
||||||
return ["descendant", self.convert_selector_to_json(selector.ancestor), self.convert_selector_to_json(selector.descendant)]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_selector_from_json(self, selector_list):
|
|
||||||
if selector_list[0] == "tag":
|
|
||||||
selector = TagSelector(selector_list[1])
|
|
||||||
selector.priority = selector_list[2]
|
|
||||||
return selector
|
|
||||||
elif selector_list[0] == "descendant":
|
|
||||||
return DescendantSelector(self.get_selector_from_json(selector_list[1]), self.get_selector_from_json(selector_list[2]))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_json(self, rules_list: list[tuple[TagSelector | DescendantSelector, dict[str, str]]]):
|
|
||||||
return [[self.convert_selector_to_json(rule[0]), rule[1]] for rule in rules_list]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_json(self, rules_list):
|
|
||||||
return [(self.get_selector_from_json(rule[0]), rule[1]) for rule in rules_list]
|
|
||||||
|
|
||||||
def style(node, rules):
|
|
||||||
node.style = {}
|
|
||||||
|
|
||||||
for property, default_value in INHERITED_PROPERTIES.items():
|
|
||||||
if node.parent:
|
|
||||||
node.style[property] = node.parent.style[property]
|
|
||||||
else:
|
|
||||||
node.style[property] = default_value
|
|
||||||
|
|
||||||
for selector, body in rules:
|
|
||||||
if not selector.matches(node): continue
|
|
||||||
for property, value in body.items():
|
|
||||||
node.style[property] = value
|
|
||||||
|
|
||||||
if isinstance(node, Element) and "style" in node.attributes:
|
|
||||||
pairs = CSSParser(node.attributes["style"]).body()
|
|
||||||
for property, value in pairs.items():
|
|
||||||
node.style[property] = value
|
|
||||||
|
|
||||||
if node.style["font-size"].endswith("%"):
|
|
||||||
if node.parent:
|
|
||||||
parent_font_size = node.parent.style["font-size"]
|
|
||||||
else:
|
|
||||||
parent_font_size = INHERITED_PROPERTIES["font-size"]
|
|
||||||
|
|
||||||
node_pct = float(node.style["font-size"][:-1]) / 100
|
|
||||||
parent_px = float(parent_font_size[:-2])
|
|
||||||
node.style["font-size"] = str(node_pct * parent_px) + "px"
|
|
||||||
|
|
||||||
for child in node.children:
|
|
||||||
style(child, rules)
|
|
||||||
|
|
||||||
def tree_to_list(tree, list):
|
|
||||||
list.append(tree)
|
|
||||||
for child in tree.children:
|
|
||||||
tree_to_list(child, list)
|
|
||||||
return list
|
|
||||||
|
|
||||||
def replace_symbols(text):
|
|
||||||
for key, value in html.entities.html5.items():
|
|
||||||
text = text.replace(f"&{key};", value)
|
|
||||||
|
|
||||||
return text
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
import arcade, pyglet, platform
|
|
||||||
|
|
||||||
from utils.constants import BLOCK_ELEMENTS, token_pattern, emoji_pattern, INHERITED_PROPERTIES
|
|
||||||
from utils.utils import get_color_from_name, hex_to_rgb
|
|
||||||
|
|
||||||
from http_client.connection import HTTPClient
|
|
||||||
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority
|
|
||||||
|
|
||||||
from pyglet.font.base import Font as BaseFont
|
|
||||||
|
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
HSTEP = 13
|
|
||||||
VSTEP = 18
|
|
||||||
|
|
||||||
SPACE_MULTIPLIER = 0.25 if not platform.system() == "Windows" else 0.33
|
|
||||||
|
|
||||||
font_cache = {}
|
|
||||||
|
|
||||||
def ensure_font(font_family, size, weight, style, emoji):
|
|
||||||
if not (font_family, size, weight, style, emoji) in font_cache:
|
|
||||||
font_cache[(font_family, size, weight, style, emoji)] = pyglet.font.load(font_family if pyglet.font.have_font(font_family) else "Arial", size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic")
|
|
||||||
|
|
||||||
return font_cache[(font_family, size, weight, style, emoji)]
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def get_space_width(font: BaseFont):
|
|
||||||
return font.get_text_size("a")[0] * SPACE_MULTIPLIER # i have to do this because space width is 0 on Windows (of course, why wouldnt Windows ruin everything?)
|
|
||||||
|
|
||||||
class DrawText:
|
|
||||||
def __init__(self, x1, y1, text, font, color):
|
|
||||||
self.top = y1
|
|
||||||
self.left = x1
|
|
||||||
self.text = text
|
|
||||||
self.font = font
|
|
||||||
self.color = color
|
|
||||||
|
|
||||||
class DrawRect:
|
|
||||||
def __init__(self, x1, y1, width, height, color):
|
|
||||||
self.top = y1
|
|
||||||
self.left = x1
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.color = color
|
|
||||||
|
|
||||||
class LineLayout:
|
|
||||||
def __init__(self, node, parent, previous):
|
|
||||||
self.node = node
|
|
||||||
self.parent = parent
|
|
||||||
self.previous = previous
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
def paint(self):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def layout(self):
|
|
||||||
self.width = self.parent.width
|
|
||||||
self.x = self.parent.x
|
|
||||||
|
|
||||||
if self.previous:
|
|
||||||
self.y = self.previous.y + self.previous.height
|
|
||||||
else:
|
|
||||||
self.y = self.parent.y
|
|
||||||
|
|
||||||
for word in self.children:
|
|
||||||
word.layout()
|
|
||||||
|
|
||||||
if not self.children:
|
|
||||||
self.height = 0
|
|
||||||
return
|
|
||||||
|
|
||||||
fonts_on_line = [word.font for word in self.children]
|
|
||||||
max_ascent = max(font.ascent for font in fonts_on_line)
|
|
||||||
|
|
||||||
baseline = self.y + 2 * max_ascent
|
|
||||||
|
|
||||||
for word in self.children:
|
|
||||||
word.y = baseline - word.font.ascent
|
|
||||||
|
|
||||||
max_descent = min(font.descent for font in fonts_on_line)
|
|
||||||
|
|
||||||
self.height = 2 * (max_ascent + max_descent)
|
|
||||||
|
|
||||||
class TextLayout():
|
|
||||||
def __init__(self, node, word, emoji, parent, previous):
|
|
||||||
self.node = node
|
|
||||||
self.word = word
|
|
||||||
self.children = []
|
|
||||||
self.parent = parent
|
|
||||||
self.previous = previous
|
|
||||||
self.emoji = emoji
|
|
||||||
|
|
||||||
def paint(self):
|
|
||||||
color = get_color_from_name(self.node.style["color"])
|
|
||||||
return [DrawText(self.x, self.y, self.word, self.font, color)]
|
|
||||||
|
|
||||||
def layout(self):
|
|
||||||
weight = self.node.style["font-weight"]
|
|
||||||
style = self.node.style["font-style"]
|
|
||||||
font_family = self.node.style["font-family"]
|
|
||||||
style = "roman" if style == "normal" else style
|
|
||||||
if not self.node.style["font-size"].endswith("em") and not self.node.style["font-size"].endswith("rem"):
|
|
||||||
size = int(float(self.node.style["font-size"][:-2]))
|
|
||||||
else:
|
|
||||||
size = int(INHERITED_PROPERTIES["font-size"][:-2])
|
|
||||||
self.font = ensure_font(font_family, size, weight, style, self.emoji)
|
|
||||||
|
|
||||||
self.width = self.font.get_text_size(self.word + (" " if not self.emoji else " "))[0]
|
|
||||||
|
|
||||||
if self.previous:
|
|
||||||
space = get_space_width(self.previous.font)
|
|
||||||
self.x = self.previous.x + space + self.previous.width
|
|
||||||
else:
|
|
||||||
self.x = self.parent.x
|
|
||||||
|
|
||||||
self.height = self.font.ascent + self.font.descent
|
|
||||||
|
|
||||||
class BlockLayout:
|
|
||||||
def __init__(self, node, parent, previous):
|
|
||||||
self.node = node
|
|
||||||
self.parent = parent
|
|
||||||
self.previous = previous
|
|
||||||
|
|
||||||
self.children = []
|
|
||||||
self.cursor_x = 0
|
|
||||||
|
|
||||||
self.x, self.y, self.width, self.height = None, None, None, None
|
|
||||||
|
|
||||||
def paint(self):
|
|
||||||
cmds = []
|
|
||||||
if self.layout_mode() == "inline":
|
|
||||||
bgcolor = self.node.style.get("background-color", "transparent")
|
|
||||||
if bgcolor != "transparent":
|
|
||||||
rect = DrawRect(self.x, self.y, self.width, self.height, hex_to_rgb(bgcolor) if bgcolor.startswith("#") else get_color_from_name(bgcolor))
|
|
||||||
cmds.append(rect)
|
|
||||||
|
|
||||||
return cmds
|
|
||||||
|
|
||||||
def layout_mode(self):
|
|
||||||
if isinstance(self.node, Text):
|
|
||||||
return "inline"
|
|
||||||
elif any([isinstance(child, Element) and child.tag in BLOCK_ELEMENTS for child in self.node.children]):
|
|
||||||
return "block"
|
|
||||||
elif self.node.children:
|
|
||||||
return "inline"
|
|
||||||
else:
|
|
||||||
return "block"
|
|
||||||
|
|
||||||
def layout(self):
|
|
||||||
self.x = self.parent.x
|
|
||||||
self.width = self.parent.width
|
|
||||||
|
|
||||||
if self.previous:
|
|
||||||
self.y = self.previous.y + self.previous.height
|
|
||||||
else:
|
|
||||||
self.y = self.parent.y
|
|
||||||
|
|
||||||
mode = self.layout_mode()
|
|
||||||
if mode == "block":
|
|
||||||
previous = None
|
|
||||||
for child in self.node.children:
|
|
||||||
next = BlockLayout(child, self, previous)
|
|
||||||
self.children.append(next)
|
|
||||||
previous = next
|
|
||||||
else:
|
|
||||||
self.new_line()
|
|
||||||
self.recurse(self.node)
|
|
||||||
|
|
||||||
for child in self.children:
|
|
||||||
child.layout()
|
|
||||||
|
|
||||||
self.height = sum([child.height for child in self.children])
|
|
||||||
|
|
||||||
def recurse(self, node):
|
|
||||||
if isinstance(node, Text):
|
|
||||||
word_list = [match.group(0) for match in token_pattern.finditer(node.text)]
|
|
||||||
|
|
||||||
for word in word_list:
|
|
||||||
if emoji_pattern.fullmatch(word):
|
|
||||||
self.word(self.node, word, emoji=True)
|
|
||||||
else:
|
|
||||||
self.word(self.node, word)
|
|
||||||
else:
|
|
||||||
if node.tag == "br":
|
|
||||||
self.new_line()
|
|
||||||
|
|
||||||
for child in node.children:
|
|
||||||
self.recurse(child)
|
|
||||||
|
|
||||||
def word(self, node, word: str, emoji=False):
|
|
||||||
weight = node.style["font-weight"]
|
|
||||||
style = node.style["font-style"]
|
|
||||||
font_family = node.style["font-family"]
|
|
||||||
style = "roman" if style == "normal" else style
|
|
||||||
if not node.style["font-size"].endswith("em") and not node.style["font-size"].endswith("rem"):
|
|
||||||
size = int(float(node.style["font-size"][:-2]))
|
|
||||||
else:
|
|
||||||
size = int(INHERITED_PROPERTIES["font-size"][:-2])
|
|
||||||
|
|
||||||
font = ensure_font(font_family, size, weight, style, emoji)
|
|
||||||
|
|
||||||
w = font.get_text_size(word + (" " if not emoji else " "))[0]
|
|
||||||
|
|
||||||
if self.cursor_x + w > self.width:
|
|
||||||
self.new_line()
|
|
||||||
|
|
||||||
line = self.children[-1]
|
|
||||||
previous_word = line.children[-1] if line.children else None
|
|
||||||
text = TextLayout(node, word, emoji, line, previous_word)
|
|
||||||
line.children.append(text)
|
|
||||||
|
|
||||||
self.cursor_x += w + get_space_width(font)
|
|
||||||
|
|
||||||
def new_line(self):
|
|
||||||
self.cursor_x = 0
|
|
||||||
|
|
||||||
last_line = self.children[-1] if self.children else None
|
|
||||||
new_line = LineLayout(self.node, self, last_line)
|
|
||||||
|
|
||||||
self.children.append(new_line)
|
|
||||||
|
|
||||||
class DocumentLayout:
|
|
||||||
def __init__(self, node):
|
|
||||||
self.node = node
|
|
||||||
self.parent = None
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
def layout(self):
|
|
||||||
child = BlockLayout(self.node, self, None)
|
|
||||||
self.children.append(child)
|
|
||||||
|
|
||||||
self.width = arcade.get_window().width - 2 * HSTEP
|
|
||||||
self.x = HSTEP
|
|
||||||
self.y = VSTEP
|
|
||||||
child.layout()
|
|
||||||
self.height = child.height
|
|
||||||
|
|
||||||
def paint(self):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def paint_tree(layout_object, display_list):
|
|
||||||
display_list.extend(layout_object.paint())
|
|
||||||
|
|
||||||
for child in layout_object.children:
|
|
||||||
paint_tree(child, display_list)
|
|
||||||
|
|
||||||
class Renderer():
|
|
||||||
def __init__(self, http_client: HTTPClient, window):
|
|
||||||
self.content = ''
|
|
||||||
self.request_scheme = 'http'
|
|
||||||
self.window: arcade.Window = window
|
|
||||||
self.current_window_size = self.window.size
|
|
||||||
self.http_client = http_client
|
|
||||||
|
|
||||||
self.scroll_y = 0
|
|
||||||
self.scroll_y_speed = 50
|
|
||||||
self.allow_scroll = False
|
|
||||||
self.smallest_y = 0
|
|
||||||
self.document = None
|
|
||||||
|
|
||||||
self.widgets: list[pyglet.text.Label] = []
|
|
||||||
self.text_to_create = []
|
|
||||||
|
|
||||||
self.batch = pyglet.graphics.Batch()
|
|
||||||
|
|
||||||
def hide_out_of_bounds_labels(self):
|
|
||||||
for widget in self.widgets:
|
|
||||||
invisible = (widget.y + (widget.content_height if not isinstance(widget, pyglet.shapes.Rectangle) else widget.height)) > self.window.height * 0.925
|
|
||||||
# Doing visible flag set manually since it takes a lot of time
|
|
||||||
if widget.visible:
|
|
||||||
if invisible:
|
|
||||||
widget.visible = False
|
|
||||||
else:
|
|
||||||
if not invisible:
|
|
||||||
widget.visible = True
|
|
||||||
|
|
||||||
def on_resize(self, width, height):
|
|
||||||
self.current_window_size = self.window.size
|
|
||||||
self.http_client.needs_render = True
|
|
||||||
|
|
||||||
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
|
|
||||||
if not self.allow_scroll:
|
|
||||||
return
|
|
||||||
|
|
||||||
old_y = self.scroll_y
|
|
||||||
self.scroll_y = max(0, min(abs(self.scroll_y - (scroll_y * self.scroll_y_speed)), abs(self.smallest_y) - (self.window.height * 0.925) + 5)) # flip scroll direction
|
|
||||||
|
|
||||||
for widget in self.widgets:
|
|
||||||
widget.y += (self.scroll_y - old_y)
|
|
||||||
|
|
||||||
self.hide_out_of_bounds_labels()
|
|
||||||
|
|
||||||
def add_text(self, x, y, text, font, color, multiline=False):
|
|
||||||
self.widgets.append(
|
|
||||||
pyglet.text.Label(
|
|
||||||
text=text,
|
|
||||||
font_name=font.name,
|
|
||||||
italic=font.italic,
|
|
||||||
weight=font.weight,
|
|
||||||
font_size=font.size,
|
|
||||||
width=self.window.width * 0.5 if multiline else None,
|
|
||||||
multiline=multiline,
|
|
||||||
color=color,
|
|
||||||
x=x,
|
|
||||||
y=(self.window.height * 0.925) - y,
|
|
||||||
batch=self.batch
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (self.window.height * 0.925) - y < self.smallest_y:
|
|
||||||
self.smallest_y = y
|
|
||||||
|
|
||||||
def add_background(self, left, top, width, height, color):
|
|
||||||
self.widgets.append(
|
|
||||||
pyglet.shapes.Rectangle(
|
|
||||||
left,
|
|
||||||
(self.window.height * 0.925) - top - height,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
color,
|
|
||||||
batch=self.batch
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
if not self.http_client.needs_render:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.http_client.needs_render = False
|
|
||||||
self.allow_scroll = True
|
|
||||||
|
|
||||||
for child in self.widgets:
|
|
||||||
child.delete()
|
|
||||||
del child
|
|
||||||
|
|
||||||
self.widgets.clear()
|
|
||||||
self.smallest_y = 0
|
|
||||||
|
|
||||||
if self.http_client.view_source or self.http_client.scheme == "file":
|
|
||||||
self.add_text(x=HSTEP, y=self.window.height * 0.05, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), color=arcade.color.BLACK, multiline=True)
|
|
||||||
|
|
||||||
elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
|
|
||||||
if self.http_client.nodes:
|
|
||||||
style(self.http_client.nodes, sorted(self.http_client.css_rules + CSSParser(open("assets/css/browser.css").read()).parse(), key=cascade_priority))
|
|
||||||
|
|
||||||
self.document = DocumentLayout(self.http_client.nodes)
|
|
||||||
self.document.layout()
|
|
||||||
self.cmds = []
|
|
||||||
paint_tree(self.document, self.cmds)
|
|
||||||
|
|
||||||
for cmd in self.cmds:
|
|
||||||
if isinstance(cmd, DrawText):
|
|
||||||
self.add_text(cmd.left, cmd.top, cmd.text, cmd.font, cmd.color)
|
|
||||||
elif isinstance(cmd, DrawRect):
|
|
||||||
self.add_background(cmd.left, cmd.top, cmd.width, cmd.height, cmd.color)
|
|
||||||
|
|
||||||
self.hide_out_of_bounds_labels()
|
|
||||||
181
menus/main.py
181
menus/main.py
@@ -1,181 +0,0 @@
|
|||||||
import arcade, arcade.gui, asyncio, pypresence, time, copy, json, asyncio
|
|
||||||
|
|
||||||
from utils.constants import discord_presence_id, DEFAULT_HEADERS
|
|
||||||
from utils.utils import FakePyPresence
|
|
||||||
|
|
||||||
from http_client.connection import HTTPClient, resolve_url
|
|
||||||
from http_client.html_parser import tree_to_list, Text
|
|
||||||
from http_client.renderer import Renderer
|
|
||||||
|
|
||||||
class Tab():
|
|
||||||
def __init__(self, url, window, tab_button, pypresence_client):
|
|
||||||
self.pypresence_client = pypresence_client
|
|
||||||
self.tab_button = tab_button
|
|
||||||
self.window = window
|
|
||||||
self.http_client = HTTPClient()
|
|
||||||
self.renderer = Renderer(self.http_client, window)
|
|
||||||
|
|
||||||
self.request(url)
|
|
||||||
|
|
||||||
def request(self, url):
|
|
||||||
if url.startswith("http://") or url.startswith("https://") or url.startswith("view-source:"):
|
|
||||||
self.http_client.get_request(url, DEFAULT_HEADERS)
|
|
||||||
elif url.startswith("file://"):
|
|
||||||
self.http_client.file_request(url)
|
|
||||||
elif url.startswith("data:text/html,"):
|
|
||||||
self.http_client.content_response = url.split("data:text/html,")[1]
|
|
||||||
self.http_client.scheme = "http"
|
|
||||||
elif url == "about:blank":
|
|
||||||
self.http_client.content_response = ""
|
|
||||||
self.http_client.scheme = "http"
|
|
||||||
elif url == "about:config" or url == "about:settings":
|
|
||||||
self.settings()
|
|
||||||
else:
|
|
||||||
self.http_client.get_request(f"https://{url}", DEFAULT_HEADERS)
|
|
||||||
|
|
||||||
self.tab_button.text = url
|
|
||||||
|
|
||||||
def settings(self):
|
|
||||||
from menus.settings import Settings
|
|
||||||
self.window.show_view(Settings(self.pypresence_client))
|
|
||||||
|
|
||||||
class Main(arcade.gui.UIView):
|
|
||||||
def __init__(self, pypresence_client=None):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.pypresence_client = pypresence_client
|
|
||||||
|
|
||||||
with open("settings.json", "r") as file:
|
|
||||||
self.settings_dict = json.load(file)
|
|
||||||
|
|
||||||
if self.settings_dict.get('discord_rpc', True):
|
|
||||||
if self.pypresence_client == None: # Game has started
|
|
||||||
try:
|
|
||||||
asyncio.get_event_loop()
|
|
||||||
except:
|
|
||||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
||||||
try:
|
|
||||||
self.pypresence_client = pypresence.Presence(discord_presence_id)
|
|
||||||
self.pypresence_client.connect()
|
|
||||||
self.pypresence_client.start_time = time.time()
|
|
||||||
except:
|
|
||||||
self.pypresence_client = FakePyPresence()
|
|
||||||
self.pypresence_client.start_time = time.time()
|
|
||||||
|
|
||||||
elif isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
|
|
||||||
# get start time from old object
|
|
||||||
start_time = copy.deepcopy(self.pypresence_client.start_time)
|
|
||||||
try:
|
|
||||||
self.pypresence_client = pypresence.Presence(discord_presence_id)
|
|
||||||
self.pypresence_client.connect()
|
|
||||||
self.pypresence_client.start_time = start_time
|
|
||||||
except:
|
|
||||||
self.pypresence_client = FakePyPresence()
|
|
||||||
self.pypresence_client.start_time = start_time
|
|
||||||
|
|
||||||
self.pypresence_client.update(state='Browsing', details='In the browser', start=self.pypresence_client.start_time)
|
|
||||||
else: # game has started, but the user has disabled RPC in the settings.
|
|
||||||
self.pypresence_client = FakePyPresence()
|
|
||||||
self.pypresence_client.start_time = time.time()
|
|
||||||
|
|
||||||
self.pypresence_client.update(state='Browsing', details='In the browser', start=self.pypresence_client.start_time)
|
|
||||||
|
|
||||||
self.tabs: list[Tab] = []
|
|
||||||
self.tab_buttons = []
|
|
||||||
self.active_tab = None
|
|
||||||
|
|
||||||
def on_show_view(self):
|
|
||||||
super().on_show_view()
|
|
||||||
|
|
||||||
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
|
|
||||||
self.navigation_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x="center", anchor_y="top")
|
|
||||||
|
|
||||||
self.tab_box = self.navigation_box.add(arcade.gui.UIBoxLayout(space_between=5, vertical=False))
|
|
||||||
|
|
||||||
self.new_tab_button = self.tab_box.add(arcade.gui.UIFlatButton(text="+", width=self.window.width / 25, height=30, style=arcade.gui.UIFlatButton.DEFAULT_STYLE))
|
|
||||||
self.new_tab_button.on_click = lambda event: self.new_tab()
|
|
||||||
|
|
||||||
self.tab_buttons.append(self.tab_box.add(arcade.gui.UIFlatButton(text="about:blank", width=self.window.width / 7, height=30, style=arcade.gui.UIFlatButton.STYLE_BLUE)))
|
|
||||||
|
|
||||||
self.search_bar = self.navigation_box.add(arcade.gui.UIInputText(width=self.window.width * 0.5, height=30, font_name="Roboto", font_size=14, text_color=arcade.color.BLACK, caret_color=arcade.color.BLACK, border_color=arcade.color.BLACK))
|
|
||||||
|
|
||||||
default_tab = Tab("about:blank", self.window, self.tab_buttons[0], self.pypresence_client)
|
|
||||||
self.tabs.append(default_tab)
|
|
||||||
self.tab_buttons[-1].on_click = lambda event, tab=self.tabs[0]: self.switch_to_tab(tab)
|
|
||||||
|
|
||||||
self.switch_to_tab(default_tab)
|
|
||||||
|
|
||||||
def search(self):
|
|
||||||
url = self.search_bar.text
|
|
||||||
|
|
||||||
self.active_tab.request(url)
|
|
||||||
|
|
||||||
def switch_to_tab(self, tab):
|
|
||||||
if self.active_tab:
|
|
||||||
self.active_tab.tab_button.style = arcade.gui.UIFlatButton.DEFAULT_STYLE
|
|
||||||
|
|
||||||
self.active_tab = tab
|
|
||||||
self.active_tab.tab_button.style = arcade.gui.UIFlatButton.STYLE_BLUE
|
|
||||||
|
|
||||||
if self.active_tab.renderer.current_window_size != self.window.size:
|
|
||||||
self.active_tab.renderer.on_resize(self.window.width, self.window.height)
|
|
||||||
|
|
||||||
self.window.on_mouse_scroll = self.active_tab.renderer.on_mouse_scroll
|
|
||||||
|
|
||||||
http_client = self.active_tab.http_client
|
|
||||||
if http_client.scheme and http_client.host and http_client.path:
|
|
||||||
port_str = f':{http_client.port}' if not http_client.port in [80, 443, 0] else ''
|
|
||||||
self.search_bar.text = f"{http_client.scheme}://{http_client.host}{port_str}{http_client.path}"
|
|
||||||
|
|
||||||
def new_tab(self, url="about:blank"):
|
|
||||||
self.tab_buttons.append(self.tab_box.add(arcade.gui.UIFlatButton(text=url, width=self.window.width / 7, height=30, style=arcade.gui.UIFlatButton.STYLE_BLUE)))
|
|
||||||
self.tabs.append(Tab(url, self.window, self.tab_buttons[-1], self.pypresence_client))
|
|
||||||
self.tab_buttons[-1].on_click = lambda event, tab=self.tabs[-1]: self.switch_to_tab(tab)
|
|
||||||
|
|
||||||
self.switch_to_tab(self.tabs[-1])
|
|
||||||
|
|
||||||
def on_resize(self, width, height):
|
|
||||||
for tab_button in self.tab_buttons:
|
|
||||||
tab_button.rect = tab_button.rect.resize(width / 7, 30)
|
|
||||||
|
|
||||||
self.active_tab.renderer.on_resize(width, height)
|
|
||||||
|
|
||||||
def on_key_press(self, symbol, modifiers):
|
|
||||||
self.search_bar.text = self.search_bar.text.encode("ascii", "ignore").decode().strip("\n")
|
|
||||||
if symbol == arcade.key.ENTER:
|
|
||||||
self.search()
|
|
||||||
|
|
||||||
def on_update(self, delta_time):
|
|
||||||
self.active_tab.renderer.update()
|
|
||||||
|
|
||||||
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
|
|
||||||
if not self.active_tab.renderer.document:
|
|
||||||
return
|
|
||||||
|
|
||||||
y -= self.active_tab.renderer.scroll_y
|
|
||||||
|
|
||||||
objs = [
|
|
||||||
obj for obj in tree_to_list(self.active_tab.renderer.document, [])
|
|
||||||
if obj.x <= x < obj.x + obj.width
|
|
||||||
and ((self.window.height * 0.925) - obj.y - obj.height) <= y < ((self.window.height * 0.925) - obj.y)
|
|
||||||
]
|
|
||||||
|
|
||||||
if not objs:
|
|
||||||
return
|
|
||||||
|
|
||||||
elt = objs[-1].node
|
|
||||||
|
|
||||||
while elt:
|
|
||||||
if isinstance(elt, Text):
|
|
||||||
pass
|
|
||||||
elif elt.tag == "a" and "href" in elt.attributes:
|
|
||||||
url = resolve_url(self.active_tab.http_client.scheme, self.active_tab.http_client.host, self.active_tab.http_client.port, self.active_tab.http_client.path, elt.attributes["href"])
|
|
||||||
self.new_tab(url)
|
|
||||||
return
|
|
||||||
|
|
||||||
elt = elt.parent
|
|
||||||
|
|
||||||
def on_draw(self):
|
|
||||||
super().on_draw()
|
|
||||||
|
|
||||||
self.active_tab.renderer.batch.draw()
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
import copy, pypresence, json
|
|
||||||
|
|
||||||
import arcade, arcade.gui
|
|
||||||
|
|
||||||
from utils.constants import button_style, dropdown_style, slider_style, settings, discord_presence_id, settings_start_category
|
|
||||||
from utils.utils import FakePyPresence
|
|
||||||
from utils.preload import button_texture, button_hovered_texture
|
|
||||||
|
|
||||||
from arcade.gui import UIBoxLayout, UIAnchorLayout
|
|
||||||
|
|
||||||
class Settings(arcade.gui.UIView):
|
|
||||||
def __init__(self, pypresence_client):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
with open("settings.json", "r") as file:
|
|
||||||
self.settings_dict = json.load(file)
|
|
||||||
|
|
||||||
self.pypresence_client = pypresence_client
|
|
||||||
self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=self.pypresence_client.start_time)
|
|
||||||
|
|
||||||
self.slider_labels = {}
|
|
||||||
self.sliders = {}
|
|
||||||
|
|
||||||
self.on_radiobuttons = {}
|
|
||||||
self.off_radiobuttons = {}
|
|
||||||
|
|
||||||
self.current_category = settings_start_category
|
|
||||||
|
|
||||||
self.modified_settings = {}
|
|
||||||
|
|
||||||
def create_layouts(self):
|
|
||||||
self.anchor = self.add_widget(UIAnchorLayout(size_hint=(1, 1)))
|
|
||||||
|
|
||||||
self.box = UIBoxLayout(space_between=50, align="center", vertical=False)
|
|
||||||
self.anchor.add(self.box, anchor_x="center", anchor_y="top", align_x=10, align_y=-75)
|
|
||||||
|
|
||||||
self.top_box = UIBoxLayout(space_between=self.window.width / 160, vertical=False)
|
|
||||||
self.anchor.add(self.top_box, anchor_x="left", anchor_y="top", align_x=10, align_y=-10)
|
|
||||||
|
|
||||||
self.key_layout = self.box.add(UIBoxLayout(space_between=20, align='left'))
|
|
||||||
self.value_layout = self.box.add(UIBoxLayout(space_between=13, align='left'))
|
|
||||||
|
|
||||||
def on_show_view(self):
|
|
||||||
super().on_show_view()
|
|
||||||
|
|
||||||
self.create_layouts()
|
|
||||||
|
|
||||||
self.ui.push_handlers(self)
|
|
||||||
|
|
||||||
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
|
|
||||||
self.back_button.on_click = lambda event: self.main_exit()
|
|
||||||
self.top_box.add(self.back_button)
|
|
||||||
|
|
||||||
self.display_categories()
|
|
||||||
|
|
||||||
self.display_category(settings_start_category)
|
|
||||||
|
|
||||||
def display_categories(self):
|
|
||||||
for category in settings:
|
|
||||||
category_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=category, style=button_style, width=self.window.width / 10, height=50)
|
|
||||||
|
|
||||||
if not category == "Credits":
|
|
||||||
category_button.on_click = lambda event, category=category: self.display_category(category)
|
|
||||||
else:
|
|
||||||
category_button.on_click = lambda event: self.credits()
|
|
||||||
|
|
||||||
self.top_box.add(category_button)
|
|
||||||
|
|
||||||
def display_category(self, category):
|
|
||||||
if hasattr(self, 'apply_button'):
|
|
||||||
self.anchor.remove(self.apply_button)
|
|
||||||
del self.apply_button
|
|
||||||
|
|
||||||
if hasattr(self, 'credits_label'):
|
|
||||||
self.anchor.remove(self.credits_label)
|
|
||||||
del self.credits_label
|
|
||||||
|
|
||||||
self.current_category = category
|
|
||||||
|
|
||||||
self.key_layout.clear()
|
|
||||||
self.value_layout.clear()
|
|
||||||
|
|
||||||
for setting in settings[category]:
|
|
||||||
label = arcade.gui.UILabel(text=setting, font_name="Roboto", font_size=28, text_color=arcade.color.BLACK )
|
|
||||||
self.key_layout.add(label)
|
|
||||||
|
|
||||||
setting_dict = settings[category][setting]
|
|
||||||
|
|
||||||
if setting_dict['type'] == "option":
|
|
||||||
dropdown = arcade.gui.UIDropdown(options=setting_dict['options'], width=200, height=50, default=self.settings_dict.get(setting_dict["config_key"], setting_dict["options"][0]), active_style=dropdown_style, dropdown_style=dropdown_style, primary_style=dropdown_style)
|
|
||||||
dropdown.on_change = lambda _, setting=setting, dropdown=dropdown: self.update(setting, dropdown.value, "option")
|
|
||||||
self.value_layout.add(dropdown)
|
|
||||||
|
|
||||||
elif setting_dict['type'] == "bool":
|
|
||||||
button_layout = self.value_layout.add(arcade.gui.UIBoxLayout(space_between=50, vertical=False))
|
|
||||||
|
|
||||||
on_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="ON", style=button_style, width=150, height=50)
|
|
||||||
self.on_radiobuttons[setting] = on_radiobutton
|
|
||||||
on_radiobutton.on_click = lambda _, setting=setting: self.update(setting, True, "bool")
|
|
||||||
button_layout.add(on_radiobutton)
|
|
||||||
|
|
||||||
off_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="OFF", style=button_style, width=150, height=50)
|
|
||||||
self.off_radiobuttons[setting] = off_radiobutton
|
|
||||||
off_radiobutton.on_click = lambda _, setting=setting: self.update(setting, False, "bool")
|
|
||||||
button_layout.add(off_radiobutton)
|
|
||||||
|
|
||||||
if self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]):
|
|
||||||
self.set_highlighted_style(on_radiobutton)
|
|
||||||
self.set_normal_style(off_radiobutton)
|
|
||||||
else:
|
|
||||||
self.set_highlighted_style(off_radiobutton)
|
|
||||||
self.set_normal_style(on_radiobutton)
|
|
||||||
|
|
||||||
elif setting_dict['type'] == "slider":
|
|
||||||
if setting == "FPS Limit":
|
|
||||||
if self.settings_dict.get(setting_dict["config_key"]) == 0:
|
|
||||||
label_text = "FPS Limit: Disabled"
|
|
||||||
else:
|
|
||||||
label_text = f"FPS Limit: {self.settings_dict.get(setting_dict['config_key'], setting_dict['default'])}"
|
|
||||||
else:
|
|
||||||
label_text = f"{setting}: {int(self.settings_dict.get(setting_dict['config_key'], setting_dict['default']))}"
|
|
||||||
|
|
||||||
label.text = label_text
|
|
||||||
|
|
||||||
self.slider_labels[setting] = label
|
|
||||||
|
|
||||||
slider = arcade.gui.UISlider(width=400, height=50, value=self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]), min_value=setting_dict['min'], max_value=setting_dict['max'], style=slider_style)
|
|
||||||
slider.on_change = lambda _, setting=setting, slider=slider: self.update(setting, slider.value, "slider")
|
|
||||||
|
|
||||||
self.sliders[setting] = slider
|
|
||||||
self.value_layout.add(slider)
|
|
||||||
|
|
||||||
self.apply_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Apply', style=button_style, width=200, height=100)
|
|
||||||
self.apply_button.on_click = lambda event: self.apply_settings()
|
|
||||||
self.anchor.add(self.apply_button, anchor_x="right", anchor_y="bottom", align_x=-10, align_y=10)
|
|
||||||
|
|
||||||
def apply_settings(self):
|
|
||||||
for config_key, value in self.modified_settings.items():
|
|
||||||
self.settings_dict[config_key] = value
|
|
||||||
|
|
||||||
if self.settings_dict['window_mode'] == "Fullscreen":
|
|
||||||
self.window.set_fullscreen(True)
|
|
||||||
else:
|
|
||||||
self.window.set_fullscreen(False)
|
|
||||||
width, height = map(int, self.settings_dict['resolution'].split('x'))
|
|
||||||
self.window.set_size(width, height)
|
|
||||||
|
|
||||||
if self.settings_dict['vsync']:
|
|
||||||
self.window.set_vsync(True)
|
|
||||||
display_mode = self.window.display.get_default_screen().get_mode()
|
|
||||||
refresh_rate = display_mode.rate
|
|
||||||
self.window.set_update_rate(1 / refresh_rate)
|
|
||||||
self.window.set_draw_rate(1 / refresh_rate)
|
|
||||||
|
|
||||||
elif not self.settings_dict['fps_limit'] == 0:
|
|
||||||
self.window.set_vsync(False)
|
|
||||||
self.window.set_update_rate(1 / self.settings_dict['fps_limit'])
|
|
||||||
self.window.set_draw_rate(1 / self.settings_dict['fps_limit'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.window.set_vsync(False)
|
|
||||||
self.window.set_update_rate(1 / 99999999)
|
|
||||||
self.window.set_draw_rate(1 / 99999999)
|
|
||||||
|
|
||||||
if self.settings_dict['discord_rpc']:
|
|
||||||
if isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.
|
|
||||||
start_time = copy.deepcopy(self.pypresence_client.start_time)
|
|
||||||
self.pypresence_client.close()
|
|
||||||
del self.pypresence_client
|
|
||||||
try:
|
|
||||||
self.pypresence_client = pypresence.Presence(discord_presence_id)
|
|
||||||
self.pypresence_client.connect()
|
|
||||||
self.pypresence_client.update(state='In Settings', details='Modifying Settings', start=start_time)
|
|
||||||
self.pypresence_client.start_time = start_time
|
|
||||||
except:
|
|
||||||
self.pypresence_client = FakePyPresence()
|
|
||||||
self.pypresence_client.start_time = start_time
|
|
||||||
else:
|
|
||||||
if not isinstance(self.pypresence_client, FakePyPresence):
|
|
||||||
start_time = copy.deepcopy(self.pypresence_client.start_time)
|
|
||||||
self.pypresence_client.update()
|
|
||||||
self.pypresence_client.close()
|
|
||||||
del self.pypresence_client
|
|
||||||
self.pypresence_client = FakePyPresence()
|
|
||||||
self.pypresence_client.start_time = start_time
|
|
||||||
|
|
||||||
self.ui_cleanup()
|
|
||||||
|
|
||||||
self.ui = arcade.gui.UIManager()
|
|
||||||
self.ui.enable()
|
|
||||||
|
|
||||||
self.create_layouts()
|
|
||||||
|
|
||||||
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
|
|
||||||
self.back_button.on_click = lambda event: self.main_exit()
|
|
||||||
self.top_box.add(self.back_button)
|
|
||||||
|
|
||||||
self.display_categories()
|
|
||||||
|
|
||||||
self.display_category(self.current_category)
|
|
||||||
|
|
||||||
with open("settings.json", "w") as file:
|
|
||||||
file.write(json.dumps(self.settings_dict, indent=4))
|
|
||||||
|
|
||||||
def update(self, setting=None, button_state=None, setting_type="bool"):
|
|
||||||
setting_dict = settings[self.current_category][setting]
|
|
||||||
config_key = settings[self.current_category][setting]["config_key"]
|
|
||||||
|
|
||||||
if setting_type == "option":
|
|
||||||
self.modified_settings[config_key] = button_state
|
|
||||||
|
|
||||||
elif setting_type == "bool":
|
|
||||||
self.modified_settings[config_key] = button_state
|
|
||||||
|
|
||||||
if button_state:
|
|
||||||
self.set_highlighted_style(self.on_radiobuttons[setting])
|
|
||||||
self.set_normal_style(self.off_radiobuttons[setting])
|
|
||||||
else:
|
|
||||||
self.set_highlighted_style(self.off_radiobuttons[setting])
|
|
||||||
self.set_normal_style(self.on_radiobuttons[setting])
|
|
||||||
|
|
||||||
elif setting_type == "slider":
|
|
||||||
new_value = int(button_state)
|
|
||||||
|
|
||||||
self.modified_settings[config_key] = new_value
|
|
||||||
self.sliders[setting].value = new_value
|
|
||||||
|
|
||||||
if setting == "FPS Limit":
|
|
||||||
if new_value == 0:
|
|
||||||
label_text = "FPS Limit: Disabled"
|
|
||||||
else:
|
|
||||||
label_text = f"FPS Limit: {str(new_value).rjust(8)}"
|
|
||||||
else:
|
|
||||||
label_text = f"{setting}: {str(new_value).rjust(8)}"
|
|
||||||
|
|
||||||
self.slider_labels[setting].text = label_text
|
|
||||||
|
|
||||||
def credits(self):
|
|
||||||
if hasattr(self, 'apply_button'):
|
|
||||||
self.anchor.remove(self.apply_button)
|
|
||||||
del self.apply_button
|
|
||||||
|
|
||||||
if hasattr(self, 'credits_label'):
|
|
||||||
self.anchor.remove(self.credits_label)
|
|
||||||
del self.credits_label
|
|
||||||
|
|
||||||
self.key_layout.clear()
|
|
||||||
self.value_layout.clear()
|
|
||||||
|
|
||||||
with open('CREDITS', 'r') as file:
|
|
||||||
text = file.read()
|
|
||||||
|
|
||||||
if self.window.width == 3840:
|
|
||||||
font_size = 30
|
|
||||||
elif self.window.width == 2560:
|
|
||||||
font_size = 20
|
|
||||||
elif self.window.width == 1920:
|
|
||||||
font_size = 17
|
|
||||||
elif self.window.width >= 1440:
|
|
||||||
font_size = 14
|
|
||||||
else:
|
|
||||||
font_size = 12
|
|
||||||
|
|
||||||
self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.BLACK, font_name="Roboto", font_size=font_size, align="center", multiline=True)
|
|
||||||
|
|
||||||
self.key_layout.add(self.credits_label)
|
|
||||||
|
|
||||||
def set_highlighted_style(self, element):
|
|
||||||
element.texture = button_hovered_texture
|
|
||||||
element.texture_hovered = button_texture
|
|
||||||
|
|
||||||
def set_normal_style(self, element):
|
|
||||||
element.texture_hovered = button_hovered_texture
|
|
||||||
element.texture = button_texture
|
|
||||||
|
|
||||||
def main_exit(self):
|
|
||||||
from menus.main import Main
|
|
||||||
self.window.show_view(Main(self.pypresence_client))
|
|
||||||
|
|
||||||
def ui_cleanup(self):
|
|
||||||
self.ui.clear()
|
|
||||||
del self.ui
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "csd4ni3l-browser"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "csd4ni3l-browser"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.11"
|
|
||||||
dependencies = [
|
|
||||||
"arcade==3.2.0",
|
|
||||||
"certifi>=2025.7.14",
|
|
||||||
"pypresence>=4.3.0",
|
|
||||||
"ujson>=5.10.0",
|
|
||||||
]
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# This file was autogenerated by uv via the following command:
|
|
||||||
# uv pip compile pyproject.toml -o requirements.txt
|
|
||||||
arcade==3.2.0
|
|
||||||
# via csd4ni3l-browser (pyproject.toml)
|
|
||||||
attrs==25.3.0
|
|
||||||
# via pytiled-parser
|
|
||||||
certifi==2025.7.14
|
|
||||||
# via csd4ni3l-browser (pyproject.toml)
|
|
||||||
cffi==1.17.1
|
|
||||||
# via pymunk
|
|
||||||
pillow==11.0.0
|
|
||||||
# via arcade
|
|
||||||
pycparser==2.22
|
|
||||||
# via cffi
|
|
||||||
pyglet==2.1.6
|
|
||||||
# via arcade
|
|
||||||
pymunk==6.9.0
|
|
||||||
# via arcade
|
|
||||||
pypresence==4.3.0
|
|
||||||
# via csd4ni3l-browser (pyproject.toml)
|
|
||||||
pytiled-parser==2.2.9
|
|
||||||
# via arcade
|
|
||||||
typing-extensions==4.14.1
|
|
||||||
# via pytiled-parser
|
|
||||||
ujson==5.10.0
|
|
||||||
# via csd4ni3l-browser (pyproject.toml)
|
|
||||||
103
run.py
103
run.py
@@ -1,103 +0,0 @@
|
|||||||
import os, certifi
|
|
||||||
os.environ['SSL_CERT_FILE'] = certifi.where() # Fix SSL not working
|
|
||||||
|
|
||||||
import pyglet
|
|
||||||
|
|
||||||
pyglet.options.debug_gl = False
|
|
||||||
|
|
||||||
import logging, datetime, os, json, sys, arcade
|
|
||||||
|
|
||||||
from utils.utils import get_closest_resolution, print_debug_info, on_exception
|
|
||||||
from utils.constants import log_dir, menu_background_color
|
|
||||||
|
|
||||||
from menus.main import Main
|
|
||||||
|
|
||||||
sys.excepthook = on_exception
|
|
||||||
|
|
||||||
pyglet.resource.path.append(os.getcwd())
|
|
||||||
pyglet.font.add_directory('./assets/fonts')
|
|
||||||
|
|
||||||
if not os.path.exists(log_dir):
|
|
||||||
os.makedirs(log_dir)
|
|
||||||
|
|
||||||
if not os.path.exists("html_cache"):
|
|
||||||
os.makedirs("html_cache")
|
|
||||||
|
|
||||||
if not os.path.exists("css_cache"):
|
|
||||||
os.makedirs("css_cache")
|
|
||||||
|
|
||||||
while len(os.listdir(log_dir)) >= 5:
|
|
||||||
files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
|
|
||||||
oldest_file = sorted(files, key=lambda x: x[1])[0][0]
|
|
||||||
os.remove(os.path.join(log_dir, oldest_file))
|
|
||||||
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
||||||
log_filename = f"debug_{timestamp}.log"
|
|
||||||
logging.basicConfig(filename=f'{os.path.join(log_dir, log_filename)}', format='%(asctime)s %(name)s %(levelname)s: %(message)s', level=logging.DEBUG)
|
|
||||||
|
|
||||||
for logger_name_to_disable in ['arcade', "numba"]:
|
|
||||||
logging.getLogger(logger_name_to_disable).propagate = False
|
|
||||||
logging.getLogger(logger_name_to_disable).disabled = True
|
|
||||||
|
|
||||||
if os.path.exists('settings.json'):
|
|
||||||
with open('settings.json', 'r') as settings_file:
|
|
||||||
settings = json.load(settings_file)
|
|
||||||
|
|
||||||
resolution = list(map(int, settings['resolution'].split('x')))
|
|
||||||
|
|
||||||
if not settings.get("anti_aliasing", "4x MSAA") == "None":
|
|
||||||
antialiasing = int(settings.get("anti_aliasing", "4x MSAA").split('x')[0])
|
|
||||||
else:
|
|
||||||
antialiasing = 0
|
|
||||||
|
|
||||||
fullscreen = settings['window_mode'] == 'Fullscreen'
|
|
||||||
style = arcade.Window.WINDOW_STYLE_BORDERLESS if settings['window_mode'] == 'borderless' else arcade.Window.WINDOW_STYLE_DEFAULT
|
|
||||||
vsync = settings['vsync']
|
|
||||||
fps_limit = settings['fps_limit']
|
|
||||||
else:
|
|
||||||
resolution = get_closest_resolution()
|
|
||||||
antialiasing = 4
|
|
||||||
fullscreen = False
|
|
||||||
style = arcade.Window.WINDOW_STYLE_DEFAULT
|
|
||||||
vsync = True
|
|
||||||
fps_limit = 0
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
"resolution": f"{resolution[0]}x{resolution[1]}",
|
|
||||||
"antialiasing": "4x MSAA",
|
|
||||||
"window_mode": "Windowed",
|
|
||||||
"vsync": True,
|
|
||||||
"fps_limit": 60,
|
|
||||||
"discord_rpc": True
|
|
||||||
}
|
|
||||||
|
|
||||||
with open("settings.json", "w") as file:
|
|
||||||
file.write(json.dumps(settings))
|
|
||||||
|
|
||||||
window = arcade.Window(width=resolution[0], height=resolution[1], title='csd4ni3l-browser', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=True, style=style)
|
|
||||||
|
|
||||||
if vsync:
|
|
||||||
window.set_vsync(True)
|
|
||||||
display_mode = window.display.get_default_screen().get_mode()
|
|
||||||
refresh_rate = display_mode.rate
|
|
||||||
window.set_update_rate(1 / refresh_rate)
|
|
||||||
window.set_draw_rate(1 / refresh_rate)
|
|
||||||
elif not fps_limit == 0:
|
|
||||||
window.set_update_rate(1 / fps_limit)
|
|
||||||
window.set_draw_rate(1 / fps_limit)
|
|
||||||
else:
|
|
||||||
window.set_update_rate(1 / 99999999)
|
|
||||||
window.set_draw_rate(1 / 99999999)
|
|
||||||
|
|
||||||
arcade.set_background_color(menu_background_color)
|
|
||||||
|
|
||||||
print_debug_info()
|
|
||||||
main = Main()
|
|
||||||
|
|
||||||
window.show_view(main)
|
|
||||||
|
|
||||||
logging.debug('Game started.')
|
|
||||||
|
|
||||||
arcade.run()
|
|
||||||
|
|
||||||
logging.info('Exited with error code 0.')
|
|
||||||
70
src/constants.rs
Normal file
70
src/constants.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
// Regex is by AI
|
||||||
|
// pub const emoji_pattern = re.compile(
|
||||||
|
// r'['
|
||||||
|
// r'\U0001F300-\U0001F5FF'
|
||||||
|
// r'\U0001F600-\U0001F64F'
|
||||||
|
// r'\U0001F680-\U0001F6FF'
|
||||||
|
// r'\U0001F700-\U0001F77F'
|
||||||
|
// r'\U0001F780-\U0001F7FF'
|
||||||
|
// r'\U0001F800-\U0001F8FF'
|
||||||
|
// r'\U0001F900-\U0001F9FF'
|
||||||
|
// r'\U0001FA00-\U0001FA6F'
|
||||||
|
// r'\U0001FA70-\U0001FAFF'
|
||||||
|
// r'\u2600-\u26FF'
|
||||||
|
// r'\u2700-\u27BF'
|
||||||
|
// r']',
|
||||||
|
// flags=re.UNICODE
|
||||||
|
// )
|
||||||
|
|
||||||
|
// token_pattern = re.compile(
|
||||||
|
// f'({emoji_pattern.pattern})' // emoji
|
||||||
|
// r'|(\w+)' // word
|
||||||
|
// r'|([^\w\s])', // punctuation
|
||||||
|
// flags=re.UNICODE
|
||||||
|
// )
|
||||||
|
|
||||||
|
pub const BLOCK_ELEMENTS: [&str; 37] = [
|
||||||
|
"html", "body", "article", "section", "nav", "aside",
|
||||||
|
"h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
|
||||||
|
"footer", "address", "p", "hr", "pre", "blockquote",
|
||||||
|
"ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
|
||||||
|
"figcaption", "main", "div", "table", "form", "fieldset",
|
||||||
|
"legend", "details", "summary"
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const SELF_CLOSING_TAGS: [&str; 14] = [
|
||||||
|
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
||||||
|
"link", "meta", "param", "source", "track", "wbr",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const HEAD_TAGS: [&str; 9] = [
|
||||||
|
"base", "basefont", "bgsound", "noscript",
|
||||||
|
"link", "meta", "title", "style", "script",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const HEAD_TAGS_EXTRA: [&str; 10] = [
|
||||||
|
"base", "basefont", "bgsound", "noscript",
|
||||||
|
"link", "meta", "title", "style", "script", "/head"
|
||||||
|
];
|
||||||
|
|
||||||
|
pub static INHERITED_PROPERTIES: LazyLock<HashMap<String, String>> = LazyLock::new(|| HashMap::from([
|
||||||
|
(String::from("font-family"), String::from("Arial")),
|
||||||
|
(String::from("font-size"), String::from("16px")),
|
||||||
|
(String::from("font-style"), String::from("normal")),
|
||||||
|
(String::from("font-weight"), String::from("normal")),
|
||||||
|
(String::from("color"), String::from("black")),
|
||||||
|
(String::from("display"), String::from("inline")),
|
||||||
|
(String::from("width"), String::from("auto")),
|
||||||
|
(String::from("height"), String::from("auto"))
|
||||||
|
]));
|
||||||
|
|
||||||
|
pub static DEFAULT_HEADERS: LazyLock<HashMap<String, String>> = LazyLock::new(|| HashMap::from([
|
||||||
|
(String::from("User-Agent"), String::from("Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0")),
|
||||||
|
(String::from("Accept"), String::from("text/html")),
|
||||||
|
(String::from("Sec-Fetch-Dest"), String::from("document")),
|
||||||
|
(String::from("Sec-Fetch-Mode"), String::from("navigate")),
|
||||||
|
(String::from("Sec-Fetch-Site"), String::from("none"))
|
||||||
|
]));
|
||||||
382
src/http_client/connection.rs
Normal file
382
src/http_client/connection.rs
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
use std::{collections::HashMap, net::{Shutdown, TcpStream}, io::{Read, Write}, fs};
|
||||||
|
use native_tls::{TlsConnector, TlsStream};
|
||||||
|
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
|
||||||
|
use crate::http_client::html_parser::{CSSCache, CSSParser, CSSSelector, HTML, Node, get_inline_styles, tree_to_vec};
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
pub enum Connection {
|
||||||
|
Plain(TcpStream),
|
||||||
|
Tls(TlsStream<TcpStream>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
fn shutdown(&mut self, how: Shutdown) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Connection::Plain(s) => s.shutdown(how),
|
||||||
|
Connection::Tls(s) => {let _ = s.shutdown(); Ok(())},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for Connection {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Connection::Plain(s) => s.read(buf),
|
||||||
|
Connection::Tls(s) => s.read(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Connection {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Connection::Plain(s) => s.write(buf),
|
||||||
|
Connection::Tls(s) => s.write(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Connection::Plain(s) => s.flush(),
|
||||||
|
Connection::Tls(s) => s.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_url(scheme: &str, host: &str, port: u16, path: &str, url: &str) -> String {
|
||||||
|
let mut new_url = url;
|
||||||
|
if new_url.contains("://") {
|
||||||
|
return new_url.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolved_path = if !new_url.starts_with("/") {
|
||||||
|
let mut dir = path.rsplitn(2, '/').nth(1).unwrap_or("");
|
||||||
|
|
||||||
|
while new_url.starts_with("../") {
|
||||||
|
new_url = new_url.strip_prefix("../").unwrap();
|
||||||
|
if dir.contains('/') {
|
||||||
|
dir = dir.rsplitn(2, '/').nth(1).unwrap_or("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{}/{}", dir, new_url)
|
||||||
|
} else {
|
||||||
|
new_url.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if resolved_path.starts_with("//") {
|
||||||
|
format!("{}:{}", scheme, resolved_path)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
format!("{}://{}:{}{}", scheme, host, port, resolved_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct HTTPClient {
|
||||||
|
pub scheme: String,
|
||||||
|
pub host: String,
|
||||||
|
pub path: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub request_headers: HashMap<String, String>,
|
||||||
|
pub response_explanation: Option<String>,
|
||||||
|
pub response_headers: HashMap<String, String>,
|
||||||
|
pub response_http_version: Option<String>,
|
||||||
|
pub response_status: Option<u32>,
|
||||||
|
pub node: Option<Node>,
|
||||||
|
pub css_rules: Vec<(CSSSelector, HashMap<String, String>)>,
|
||||||
|
pub content_response: String,
|
||||||
|
pub view_source: bool,
|
||||||
|
pub redirect_count: u32,
|
||||||
|
pub needs_render: bool,
|
||||||
|
pub tcp_stream: Option<Connection>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTTPClient {
|
||||||
|
pub fn new() -> HTTPClient {
|
||||||
|
HTTPClient {
|
||||||
|
scheme: String::new(),
|
||||||
|
host: String::new(),
|
||||||
|
path: String::new(),
|
||||||
|
port: 0,
|
||||||
|
request_headers: HashMap::new(),
|
||||||
|
response_explanation: None,
|
||||||
|
response_headers: HashMap::new(),
|
||||||
|
response_http_version: None,
|
||||||
|
response_status: None,
|
||||||
|
node: None,
|
||||||
|
css_rules: Vec::new(),
|
||||||
|
content_response: String::new(),
|
||||||
|
view_source: false,
|
||||||
|
redirect_count: 0,
|
||||||
|
needs_render: false,
|
||||||
|
tcp_stream: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_request(&mut self, url: &String) {
|
||||||
|
self.content_response = fs::read_to_string(url.split_once("file://").unwrap().1).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_request(&mut self, url: &String, headers: HashMap<String, String>, css: bool) {
|
||||||
|
let mut parsed_url = url.clone();
|
||||||
|
if parsed_url.starts_with("view-source:") {
|
||||||
|
parsed_url = parsed_url.split_once("view-source:").unwrap().1.to_string();
|
||||||
|
self.view_source = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.view_source = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (scheme_str, parsed_url_parts) = parsed_url.split_once("://").unwrap();
|
||||||
|
self.scheme = scheme_str.to_string();
|
||||||
|
|
||||||
|
if !(parsed_url_parts.contains("/")) {
|
||||||
|
self.host = parsed_url_parts.to_string();
|
||||||
|
self.path = "/".to_string();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let (host_str, path_str) = parsed_url_parts.split_once("/").unwrap();
|
||||||
|
self.host = host_str.to_string();
|
||||||
|
self.path = format!("/{}", path_str.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.host.contains(":") {
|
||||||
|
let temp_host = self.host.clone();
|
||||||
|
let (host_str, port_str) = temp_host.split_once(":").unwrap();
|
||||||
|
|
||||||
|
self.host = host_str.to_string();
|
||||||
|
self.port = port_str.parse().unwrap();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if self.scheme == "http" {
|
||||||
|
self.port = 80;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.port = 443;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request_headers = headers;
|
||||||
|
self.response_explanation = None;
|
||||||
|
self.response_headers = HashMap::new();
|
||||||
|
self.response_http_version = None;
|
||||||
|
self.response_status = None;
|
||||||
|
self.content_response = "".to_string();
|
||||||
|
self.tcp_stream = None;
|
||||||
|
|
||||||
|
if self.request_headers.contains_key("Host") {
|
||||||
|
self.request_headers.remove("Host");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.request_headers.insert("Host".to_string(), self.host.clone());
|
||||||
|
|
||||||
|
let html_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, self.path).as_bytes());
|
||||||
|
let html_cache_path = format!("html_cache/{}.html", html_cache_key);
|
||||||
|
if std::fs::exists(html_cache_path.clone()).unwrap() {
|
||||||
|
self.content_response = fs::read_to_string(html_cache_path).unwrap();
|
||||||
|
self.parse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tcp = TcpStream::connect(format!("{}:{}", self.host, self.port.to_string())).unwrap();
|
||||||
|
|
||||||
|
if self.scheme == "https" {
|
||||||
|
let connector = TlsConnector::new().unwrap();
|
||||||
|
self.tcp_stream = Some(Connection::Tls(connector.connect(self.host.as_str(), tcp).unwrap()));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.tcp_stream = Some(Connection::Plain(tcp));
|
||||||
|
}
|
||||||
|
|
||||||
|
let request_header_lines: String = self.request_headers
|
||||||
|
.iter()
|
||||||
|
.map(|(header_name, header_value)|{
|
||||||
|
format!("{}: {}", header_name, header_value)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\r\n");
|
||||||
|
|
||||||
|
let request = format!("GET {} HTTP/1.0\r\n{}\r\n\r\n", self.path, request_header_lines);
|
||||||
|
|
||||||
|
self.tcp_stream.as_mut().unwrap().write_all(request.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
self.receive_response(css); // TODO: use threading
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_response(&mut self, css: bool) {
|
||||||
|
let mut temp_buffer = [0; 16384];
|
||||||
|
let mut headers_parsed: bool = false;
|
||||||
|
let mut content_length: Option<usize> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes_read = self.tcp_stream.as_mut().unwrap().read(&mut temp_buffer).unwrap_or(0);
|
||||||
|
if bytes_read == 0 {
|
||||||
|
println!("Connection closed by peer.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !headers_parsed {
|
||||||
|
let header_end_index = temp_buffer[..bytes_read].windows(4).position(|window| {window == b"\r\n\r\n"});
|
||||||
|
if let Some(header_end_index) = header_end_index {
|
||||||
|
let header_data = std::str::from_utf8(&temp_buffer[..header_end_index]).unwrap_or("");
|
||||||
|
let body_data = &temp_buffer[header_end_index + 4..bytes_read]; // +4 for the \r\n\r\n
|
||||||
|
|
||||||
|
self._parse_headers(header_data.to_string());
|
||||||
|
headers_parsed = true;
|
||||||
|
|
||||||
|
let content_length_header = self.response_headers.get("content-length");
|
||||||
|
if let Some(content_length_header) = content_length_header {
|
||||||
|
content_length = Some(content_length_header.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content_response = std::str::from_utf8(&body_data).unwrap_or("").to_string(); // Assuming body is UTF-8
|
||||||
|
|
||||||
|
if !content_length.is_none() && body_data.len() >= content_length.unwrap() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if content_length.is_none() {}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.content_response.push_str(std::str::from_utf8(&temp_buffer[..bytes_read]).unwrap_or(""));
|
||||||
|
if !content_length.is_none() && self.content_response.len() >= content_length.unwrap() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref mut stream) = self.tcp_stream {
|
||||||
|
stream.shutdown(Shutdown::Both).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tcp_stream = None;
|
||||||
|
|
||||||
|
if 300 <= self.response_status.unwrap() && self.response_status.unwrap() < 400 {
|
||||||
|
if self.redirect_count >= 4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.redirect_count += 1;
|
||||||
|
|
||||||
|
let headers = self.request_headers.clone();
|
||||||
|
let location = self.response_headers.get("location")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or("/".to_string());
|
||||||
|
if location.starts_with("http") || location.starts_with("https") {
|
||||||
|
self.get_request(&location, headers, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.get_request(&format!("{}://{}{}", self.scheme, self.host, location), headers, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.redirect_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !css {
|
||||||
|
if !(300..400).contains(&self.response_status.unwrap_or(0)) {
|
||||||
|
self.parse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _parse_headers(&mut self, header_data: String) {
|
||||||
|
let lines: Vec<&str> = header_data.lines().collect();
|
||||||
|
|
||||||
|
if lines.is_empty() {
|
||||||
|
println!("Received empty header data.");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_status_line = lines[0];
|
||||||
|
let mut parts = response_status_line.splitn(3, ' ');
|
||||||
|
|
||||||
|
self.response_http_version = Some(parts.next().unwrap().to_string());
|
||||||
|
self.response_status = Some(parts.next().unwrap().parse().unwrap());
|
||||||
|
let explanation_parts: Vec<&str> = parts.collect();
|
||||||
|
self.response_explanation = Some(explanation_parts.join(" "));
|
||||||
|
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
for i in 1..lines.len() {
|
||||||
|
let line = &lines[i];
|
||||||
|
|
||||||
|
if line.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (header_name, value) = line.split_once(":").unwrap();
|
||||||
|
headers.insert(header_name.trim().to_lowercase().to_string(), value.trim().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.response_headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(&mut self) {
|
||||||
|
self.css_rules.clear();
|
||||||
|
|
||||||
|
let html_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, self.path).as_bytes());
|
||||||
|
let html_cache_path = format!("html_cache/{}.html", html_cache_key);
|
||||||
|
|
||||||
|
if std::fs::exists(html_cache_path.clone()).unwrap() {
|
||||||
|
self.content_response = std::fs::read_to_string(html_cache_path).unwrap();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let _ = std::fs::write(html_cache_path, self.content_response.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let original_scheme = self.scheme.clone();
|
||||||
|
let original_host = self.host.clone();
|
||||||
|
let original_port = self.port;
|
||||||
|
let original_path = self.path.clone();
|
||||||
|
let original_response = self.content_response.clone();
|
||||||
|
|
||||||
|
self.node = Some(Node::Element(HTML::new(self.content_response.clone()).parse()));
|
||||||
|
|
||||||
|
let mut flattened_tree = vec![];
|
||||||
|
tree_to_vec(self.node.as_ref().unwrap(), &mut flattened_tree);
|
||||||
|
|
||||||
|
let css_links: Vec<String> = flattened_tree.iter()
|
||||||
|
.filter(|node| {
|
||||||
|
matches!(node, Node::Element(_)) && node.tag().unwrap() == "link".to_string() && node.attributes().unwrap().get("rel").unwrap() == &"stylesheet".to_string() && node.attributes().unwrap().get("href").is_some()
|
||||||
|
})
|
||||||
|
.map(|node: &&Node| {
|
||||||
|
node.attributes().unwrap()["href"].clone()
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
for css_link in css_links {
|
||||||
|
self.content_response.clear();
|
||||||
|
|
||||||
|
// we need to include the other variables so for example /styles.css wouldnt be cached for all websites
|
||||||
|
let css_cache_key = URL_SAFE.encode(format!("{}_{}_{}_{}", self.scheme, self.host, self.port, css_link).as_bytes());
|
||||||
|
let css_cache_path = format!("css_cache/{}.json", css_cache_key);
|
||||||
|
|
||||||
|
let rules: Vec<(CSSSelector, HashMap<String, String>)> = if std::path::Path::new(&css_cache_path).exists() {
|
||||||
|
let css_cache_content = std::fs::read_to_string(&css_cache_path).unwrap();
|
||||||
|
let json: CSSCache = serde_json::from_str(&css_cache_content).unwrap();
|
||||||
|
json.css_cache
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let resolved = resolve_url(self.scheme.as_str(), self.host.as_str(), self.port, self.path.as_str(), css_link.as_str());
|
||||||
|
let headers = self.request_headers.clone();
|
||||||
|
self.get_request(&resolved, headers, true);
|
||||||
|
let parsed_css = CSSParser::new(self.content_response.clone()).parse();
|
||||||
|
let json = CSSCache { css_cache: parsed_css };
|
||||||
|
let _ = std::fs::write(&css_cache_path, serde_json::to_string(&json).unwrap());
|
||||||
|
json.css_cache
|
||||||
|
};
|
||||||
|
|
||||||
|
self.css_rules.extend(rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.css_rules.extend(get_inline_styles(self.node.as_ref().unwrap()));
|
||||||
|
|
||||||
|
self.scheme = original_scheme;
|
||||||
|
self.host = original_host;
|
||||||
|
self.port = original_port;
|
||||||
|
self.path = original_path;
|
||||||
|
self.content_response = original_response;
|
||||||
|
self.needs_render = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
507
src/http_client/html_parser.rs
Normal file
507
src/http_client/html_parser.rs
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
use crate::constants::*;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Element {
|
||||||
|
pub tag: String,
|
||||||
|
pub children: Vec<Node>,
|
||||||
|
pub attributes: HashMap<String, String>,
|
||||||
|
pub parent: Option<Box<Node>>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Text {
|
||||||
|
pub text: String,
|
||||||
|
pub children: Vec<Node>,
|
||||||
|
pub parent: Option<Box<Node>>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Node {
|
||||||
|
Element(Element),
|
||||||
|
Text(Text),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn attributes(&self) -> Option<&HashMap<String, String>> {
|
||||||
|
match self {
|
||||||
|
Node::Element(e) => Some(&e.attributes),
|
||||||
|
Node::Text(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Node::Element(e) => Some(&e.tag),
|
||||||
|
Node::Text(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children(&self) -> &Vec<Node> {
|
||||||
|
match self {
|
||||||
|
Node::Element(e) => &e.children,
|
||||||
|
Node::Text(t) => &t.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent(&self) -> Option<&Node> {
|
||||||
|
match self {
|
||||||
|
Node::Text(t) => t.parent.as_deref(),
|
||||||
|
Node::Element(e) => e.parent.as_deref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HTML {
|
||||||
|
pub raw_html: String,
|
||||||
|
pub unfinished: Vec<Element>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HTML {
|
||||||
|
pub fn new(raw_html: String) -> Self {
|
||||||
|
HTML {
|
||||||
|
raw_html,
|
||||||
|
unfinished: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(&mut self) -> Element {
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut in_tag = false;
|
||||||
|
let html_content: Vec<char> = self.raw_html.chars().collect();
|
||||||
|
|
||||||
|
for c in html_content {
|
||||||
|
if c == '<' {
|
||||||
|
in_tag = true;
|
||||||
|
|
||||||
|
if (self.unfinished.is_empty() || self.unfinished.last().unwrap().tag != "style") && !text.is_empty() {
|
||||||
|
self.add_text(&text);
|
||||||
|
}
|
||||||
|
|
||||||
|
text.clear();
|
||||||
|
} else if c == '>' {
|
||||||
|
in_tag = false;
|
||||||
|
self.add_tag(&text);
|
||||||
|
text.clear();
|
||||||
|
} else {
|
||||||
|
text.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !in_tag && !text.is_empty() {
|
||||||
|
self.add_text(&text);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_text(&mut self, text: &str) {
|
||||||
|
let trimmed = text.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.implicit_tags(None);
|
||||||
|
|
||||||
|
if let Some(parent) = self.unfinished.last_mut() {
|
||||||
|
let node = Node::Text(Text {
|
||||||
|
text: text.to_string(),
|
||||||
|
parent: Some(Box::new(Node::Element(parent.clone()))),
|
||||||
|
children: Vec::new()
|
||||||
|
});
|
||||||
|
parent.children.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_attributes(&self, text: &str) -> (String, HashMap<String, String>) {
|
||||||
|
let parts: Vec<&str> = text.split_whitespace().collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return (String::new(), HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tag = parts[0].to_lowercase();
|
||||||
|
let mut attributes: HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
|
for attrpair in &parts[1..] {
|
||||||
|
if attrpair.contains('=') {
|
||||||
|
let mut split = attrpair.splitn(2, '=');
|
||||||
|
let key = split.next().unwrap_or("");
|
||||||
|
let mut value = split.next().unwrap_or("");
|
||||||
|
|
||||||
|
if value.len() > 2 && (value.starts_with('\'') || value.starts_with('"')) {
|
||||||
|
value = &value[1..value.len() - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.insert(key.to_lowercase(), value.to_string());
|
||||||
|
} else {
|
||||||
|
attributes.insert(attrpair.to_lowercase(), String::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(tag, attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_tag(&mut self, tag: &str) {
|
||||||
|
let (tag_name, attributes) = self.get_attributes(tag);
|
||||||
|
|
||||||
|
if tag_name.starts_with('!') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.implicit_tags(Some(&tag_name));
|
||||||
|
|
||||||
|
if tag_name.starts_with('/') {
|
||||||
|
if self.unfinished.len() == 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = self.unfinished.pop().unwrap();
|
||||||
|
if let Some(parent) = self.unfinished.last_mut() {
|
||||||
|
parent.children.push(Node::Element(node));
|
||||||
|
}
|
||||||
|
} else if SELF_CLOSING_TAGS.contains(&tag_name.as_str()) {
|
||||||
|
let parent_node = self.unfinished.last().map(|p| Box::new(Node::Element(p.clone())));
|
||||||
|
if let Some(parent) = self.unfinished.last_mut() {
|
||||||
|
let node = Element {
|
||||||
|
tag: tag_name,
|
||||||
|
attributes,
|
||||||
|
children: Vec::new(),
|
||||||
|
parent: parent_node,
|
||||||
|
};
|
||||||
|
parent.children.push(Node::Element(node));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let node = Element {
|
||||||
|
tag: tag_name,
|
||||||
|
attributes,
|
||||||
|
children: Vec::new(),
|
||||||
|
parent: self.unfinished.last().map(|p| Box::new(Node::Element(p.clone()))),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.unfinished.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn implicit_tags(&mut self, tag: Option<&str>) {
|
||||||
|
loop {
|
||||||
|
let open_tags: Vec<String> = self.unfinished.iter().map(|node| node.tag.clone()).collect();
|
||||||
|
|
||||||
|
if open_tags.is_empty() && tag != Some("html") {
|
||||||
|
self.add_tag("html");
|
||||||
|
} else if open_tags == vec!["html"] && !matches!(tag, Some("head") | Some("body") | Some("/html")) {
|
||||||
|
if let Some(tag_str) = tag {
|
||||||
|
if HEAD_TAGS.contains(&tag_str) {
|
||||||
|
self.add_tag("head");
|
||||||
|
} else {
|
||||||
|
self.add_tag("body");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.add_tag("body");
|
||||||
|
}
|
||||||
|
} else if open_tags == vec!["html", "head"] && !matches!(tag, Some(t) if HEAD_TAGS_EXTRA.contains(&t)) {
|
||||||
|
self.add_tag("/head");
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self) -> Element {
|
||||||
|
if self.unfinished.is_empty() {
|
||||||
|
self.implicit_tags(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
while self.unfinished.len() > 1 {
|
||||||
|
let node = self.unfinished.pop().unwrap();
|
||||||
|
if let Some(parent) = self.unfinished.last_mut() {
|
||||||
|
parent.children.push(Node::Element(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.unfinished.pop().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct TagSelector {
|
||||||
|
pub tag: String,
|
||||||
|
pub priority: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagSelector {
|
||||||
|
pub fn matches(&self, node: &Node) -> bool{
|
||||||
|
if let Node::Element(elem) = node {
|
||||||
|
self.tag == elem.tag
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(tag: String) -> TagSelector {
|
||||||
|
TagSelector {
|
||||||
|
tag,
|
||||||
|
priority: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct DescendantSelector {
|
||||||
|
pub ancestor: Box<CSSSelector>,
|
||||||
|
pub descendant: Box<CSSSelector>,
|
||||||
|
pub priority: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DescendantSelector {
|
||||||
|
pub fn new(ancestor: CSSSelector, descendant: CSSSelector) -> DescendantSelector {
|
||||||
|
let new_priority = ancestor.priority() + descendant.priority();
|
||||||
|
DescendantSelector {
|
||||||
|
ancestor: Box::new(ancestor),
|
||||||
|
descendant: Box::new(descendant),
|
||||||
|
priority: new_priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(&self, node: &Node) -> bool {
|
||||||
|
if !self.descendant.matches(node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current = node;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match current.parent() {
|
||||||
|
Some(parent) => {
|
||||||
|
if self.ancestor.matches(parent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = parent;
|
||||||
|
},
|
||||||
|
None => break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum CSSSelector {
|
||||||
|
TagSelector(TagSelector),
|
||||||
|
DescendantSelector(DescendantSelector),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CSSSelector {
|
||||||
|
pub fn priority(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
CSSSelector::TagSelector(s) => s.priority,
|
||||||
|
CSSSelector::DescendantSelector(s) => s.priority,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn matches(&self, node: &Node) -> bool {
|
||||||
|
match self {
|
||||||
|
CSSSelector::TagSelector(s) => s.matches(node),
|
||||||
|
CSSSelector::DescendantSelector(s) => s.matches(node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cascade_priority(rule: (CSSSelector, HashMap<String, String>)) -> i32 {
|
||||||
|
rule.0.priority()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_inline_styles(node: &Node) -> Vec<(CSSSelector, HashMap<String, String>)> {
|
||||||
|
let mut all_rules = vec![];
|
||||||
|
|
||||||
|
if let Node::Element(elem) = node {
|
||||||
|
for child in &elem.children {
|
||||||
|
if let Node::Element(child_elem) = child {
|
||||||
|
if child_elem.tag == "style" {
|
||||||
|
if let Some(Node::Text(text_node)) = child_elem.children.first() {
|
||||||
|
all_rules.extend(CSSParser::new(text_node.text.clone()).parse());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all_rules.extend(get_inline_styles(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all_rules
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CSSParser {
|
||||||
|
chars: Vec<char>,
|
||||||
|
len: usize,
|
||||||
|
i: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CSSParser {
|
||||||
|
pub fn new (s: String) -> CSSParser {
|
||||||
|
CSSParser {
|
||||||
|
chars: s.chars().collect(),
|
||||||
|
len: s.chars().count(),
|
||||||
|
i: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn whitespace(&mut self) {
|
||||||
|
while self.i < self.len && self.chars[self.i].is_whitespace() {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn literal(&mut self, literal: char) -> Result<(), String> {
|
||||||
|
if !(self.i < self.len && self.chars[self.i] == literal) {
|
||||||
|
return Err(format!("Expected '{}'", literal));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.i += 1;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word(&mut self) -> Result<String, String> {
|
||||||
|
let start = self.i;
|
||||||
|
|
||||||
|
while self.i < self.len {
|
||||||
|
if self.chars[self.i].is_alphanumeric() || "#-.%".contains(self.chars[self.i]) {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(self.i > start) {
|
||||||
|
return Err("Parsing error: unexpected word".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(self.chars[start..self.i].iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pair(&mut self) -> Result<(String, String), String> {
|
||||||
|
let prop = self.word()?;
|
||||||
|
|
||||||
|
self.whitespace();
|
||||||
|
self.literal(':')?;
|
||||||
|
self.whitespace();
|
||||||
|
|
||||||
|
let val = self.word()?;
|
||||||
|
|
||||||
|
Ok((prop.to_lowercase(), val))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ignore_until(&mut self, chars: Vec<char>) -> Option<char> {
|
||||||
|
while self.i < self.len {
|
||||||
|
let c = self.chars[self.i];
|
||||||
|
if chars.contains(&c) {
|
||||||
|
return Some(c);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&mut self) -> HashMap<String, String> {
|
||||||
|
let mut pairs = HashMap::new();
|
||||||
|
|
||||||
|
while self.i < self.len && self.chars[self.i] != '}' {
|
||||||
|
match self.pair() {
|
||||||
|
Ok((prop, val)) => {
|
||||||
|
pairs.insert(prop, val);
|
||||||
|
self.whitespace();
|
||||||
|
let _ = self.literal(';');
|
||||||
|
self.whitespace();
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let ignore_char: Vec<char> = vec![';', '}'];
|
||||||
|
if let Some(';') = self.ignore_until(ignore_char) {
|
||||||
|
let _ = self.literal(';');
|
||||||
|
self.whitespace();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selector(&mut self) -> Result<CSSSelector, String> {
|
||||||
|
let mut out = CSSSelector::TagSelector(TagSelector::new(self.word()?.to_lowercase()));
|
||||||
|
|
||||||
|
self.whitespace();
|
||||||
|
|
||||||
|
while self.i < self.len && self.chars[self.i] != '{' {
|
||||||
|
let tag = self.word()?;
|
||||||
|
let descendant = CSSSelector::TagSelector(TagSelector::new(tag.to_lowercase()));
|
||||||
|
out = CSSSelector::DescendantSelector(DescendantSelector::new(out, descendant));
|
||||||
|
self.whitespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(&mut self) -> Vec<(CSSSelector, HashMap<String, String>)> {
|
||||||
|
let mut rules = vec![];
|
||||||
|
while self.i < self.len {
|
||||||
|
self.whitespace();
|
||||||
|
|
||||||
|
let selector = match self.selector() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => {
|
||||||
|
if let Some('}') = self.ignore_until(vec!['}']) {
|
||||||
|
let _ = self.literal('}');
|
||||||
|
self.whitespace();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.literal('{').is_err() {
|
||||||
|
if let Some('}') = self.ignore_until(vec!['}']) {
|
||||||
|
let _ = self.literal('}');
|
||||||
|
self.whitespace();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.whitespace();
|
||||||
|
|
||||||
|
let body = self.body();
|
||||||
|
|
||||||
|
let _ = self.literal('}');
|
||||||
|
|
||||||
|
rules.push((selector, body));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tree_to_vec<'a>(tree: &'a Node, vec: &mut Vec<&'a Node>) {
|
||||||
|
vec.push(tree);
|
||||||
|
|
||||||
|
for child in tree.children() {
|
||||||
|
tree_to_vec(child, vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CSSCache {
|
||||||
|
pub css_cache: Vec<(CSSSelector, HashMap<String, String>)>
|
||||||
|
}
|
||||||
3
src/http_client/mod.rs
Normal file
3
src/http_client/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod connection;
|
||||||
|
pub mod html_parser;
|
||||||
|
pub mod renderer;
|
||||||
47
src/http_client/renderer.rs
Normal file
47
src/http_client/renderer.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use crate::http_client::connection::HTTPClient;
|
||||||
|
use bevy_egui::egui::Ui;
|
||||||
|
|
||||||
|
enum Widget {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DocumentLayout {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Renderer {
|
||||||
|
content: String,
|
||||||
|
request_scheme: String,
|
||||||
|
scroll_y: f64,
|
||||||
|
scroll_y_speed: f64,
|
||||||
|
smallest_y: f64,
|
||||||
|
document: Option<DocumentLayout>,
|
||||||
|
widgets: Vec<Widget>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
pub fn new() -> Renderer {
|
||||||
|
Renderer {
|
||||||
|
content: String::new(),
|
||||||
|
request_scheme: String::new(),
|
||||||
|
scroll_y: 0.0,
|
||||||
|
scroll_y_speed: 50.0,
|
||||||
|
smallest_y: 0.0,
|
||||||
|
document: None,
|
||||||
|
widgets: Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_content(&mut self, http_client: &mut HTTPClient) {
|
||||||
|
self.widgets.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, http_client: &mut HTTPClient, ui: &mut Ui) {
|
||||||
|
if http_client.needs_render {
|
||||||
|
self.update_content(http_client);
|
||||||
|
http_client.needs_render = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.label(http_client.content_response.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
189
src/main.rs
Normal file
189
src/main.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
use bevy::{log::Level, prelude::*};
|
||||||
|
use bevy_egui::{EguiContextSettings, EguiContexts, EguiPrimaryContextPass, EguiStartupSet, egui::{self, ecolor::Color32}};
|
||||||
|
// use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod constants;
|
||||||
|
pub mod http_client;
|
||||||
|
use crate::constants::DEFAULT_HEADERS;
|
||||||
|
use crate::http_client::{html_parser::{tree_to_vec, Node}, connection::HTTPClient, renderer::Renderer};
|
||||||
|
|
||||||
|
struct Tab {
|
||||||
|
url: String,
|
||||||
|
title: String,
|
||||||
|
http_client: HTTPClient,
|
||||||
|
renderer: Renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tab {
|
||||||
|
fn new(url: &str) -> Tab {
|
||||||
|
let http_client = HTTPClient::new();
|
||||||
|
|
||||||
|
Tab {
|
||||||
|
url: url.to_string(),
|
||||||
|
title: url.to_string(),
|
||||||
|
http_client,
|
||||||
|
renderer: Renderer::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(&mut self, url: String) {
|
||||||
|
self.url = url;
|
||||||
|
if self.url.starts_with("http://") || self.url.starts_with("https://") || self.url.starts_with("view-source:") {
|
||||||
|
self.http_client.get_request(&self.url, DEFAULT_HEADERS.clone(), false);
|
||||||
|
} else if self.url.starts_with("file://") {
|
||||||
|
self.http_client.file_request(&self.url);
|
||||||
|
} else if self.url.starts_with("data:text/html,") {
|
||||||
|
self.http_client.content_response = self.url.split("data:text/html,").nth(1).unwrap_or("").to_string();
|
||||||
|
self.http_client.scheme = "http".to_string();
|
||||||
|
} else if self.url == "about:blank" {
|
||||||
|
self.http_client.content_response = String::new();
|
||||||
|
self.http_client.scheme = "http".to_string();
|
||||||
|
} else {
|
||||||
|
self.http_client.get_request(&format!("https://{}", self.url), DEFAULT_HEADERS.clone(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_title();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_title(&mut self){
|
||||||
|
let mut flattened_tree = vec![];
|
||||||
|
tree_to_vec(&self.http_client.node.as_ref().unwrap(), &mut flattened_tree);
|
||||||
|
|
||||||
|
self.title = flattened_tree.iter()
|
||||||
|
.find(|node| {
|
||||||
|
if let Node::Text(text_node) = node {
|
||||||
|
if let Some(parent) = &text_node.parent {
|
||||||
|
return parent.tag().map(|t| t == "title").unwrap_or(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.and_then(|node| {
|
||||||
|
if let Node::Text(text_node) = node {
|
||||||
|
Some(text_node.text.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| self.url.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct AppState {
|
||||||
|
current_url: String,
|
||||||
|
active_tab: usize,
|
||||||
|
tabs: Vec<Tab>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let new_tab = Tab::new("about:blank");
|
||||||
|
|
||||||
|
let mut tabs = Vec::new();
|
||||||
|
tabs.push(new_tab);
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.insert_resource(ClearColor(Color::BLACK))
|
||||||
|
.add_plugins(
|
||||||
|
DefaultPlugins
|
||||||
|
.set(bevy::log::LogPlugin {
|
||||||
|
filter: "warn,ui=info".to_string(),
|
||||||
|
level: Level::INFO,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.set(WindowPlugin {
|
||||||
|
primary_window: Some(Window {
|
||||||
|
// You may want this set to `true` if you need virtual keyboard work in mobile browsers.
|
||||||
|
prevent_default_event_handling: false,
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.add_plugins(bevy_egui::EguiPlugin::default())
|
||||||
|
.insert_resource(AppState {
|
||||||
|
current_url: String::new(),
|
||||||
|
active_tab: 0,
|
||||||
|
tabs: tabs
|
||||||
|
})
|
||||||
|
.add_systems(
|
||||||
|
PreStartup,
|
||||||
|
setup_camera_system.before(EguiStartupSet::InitContexts),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
EguiPrimaryContextPass,
|
||||||
|
(draw, update_ui_scale_factor_system),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn update(mut app_state: ResMut<AppState>) {
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn setup_camera_system(mut commands: Commands) {
|
||||||
|
commands.spawn(Camera2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_ui_scale_factor_system(egui_context: Single<(&mut EguiContextSettings, &Camera)>) {
|
||||||
|
let (mut egui_settings, camera) = egui_context.into_inner();
|
||||||
|
egui_settings.scale_factor = 1.5 / camera.target_scaling_factor().unwrap_or(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(mut contexts: EguiContexts, mut app_state: ResMut<AppState>) -> Result {
|
||||||
|
let ctx = contexts.ctx_mut()?;
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let mut i = 0;
|
||||||
|
let mut set_active_tab_to: Option<usize> = None;
|
||||||
|
for tab in &app_state.tabs {
|
||||||
|
let mut button = egui::Button::new(tab.title.clone());
|
||||||
|
|
||||||
|
if i == app_state.active_tab {
|
||||||
|
button = button.fill(Color32::BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.add(button).clicked() {
|
||||||
|
set_active_tab_to = Some(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(set_active_tab_to) = set_active_tab_to {
|
||||||
|
// set previous's url
|
||||||
|
let active_tab = app_state.active_tab.clone();
|
||||||
|
app_state.tabs[active_tab].url = app_state.current_url.clone();
|
||||||
|
|
||||||
|
app_state.active_tab = set_active_tab_to;
|
||||||
|
|
||||||
|
// set new's url back
|
||||||
|
app_state.current_url = app_state.tabs[set_active_tab_to].url.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("+").clicked() {
|
||||||
|
let new_tab = Tab::new("about:blank");
|
||||||
|
|
||||||
|
app_state.tabs.push(new_tab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let available_height = ui.available_height();
|
||||||
|
|
||||||
|
let response = ui.add_sized([available_width, available_height / 20.0], egui::TextEdit::singleline(&mut app_state.current_url));
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||||
|
let current_url = app_state.current_url.clone();
|
||||||
|
let active_tab = app_state.active_tab;
|
||||||
|
app_state.tabs[active_tab].request(current_url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
let active_tab_index = app_state.active_tab.clone();
|
||||||
|
let tab = &mut app_state.tabs[active_tab_index];
|
||||||
|
tab.renderer.render(&mut tab.http_client, ui);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import arcade.color, re
|
|
||||||
from arcade.types import Color
|
|
||||||
from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
|
|
||||||
from arcade.gui.widgets.slider import UISliderStyle
|
|
||||||
|
|
||||||
# Regex is by AI
|
|
||||||
emoji_pattern = re.compile(
|
|
||||||
r'['
|
|
||||||
r'\U0001F300-\U0001F5FF'
|
|
||||||
r'\U0001F600-\U0001F64F'
|
|
||||||
r'\U0001F680-\U0001F6FF'
|
|
||||||
r'\U0001F700-\U0001F77F'
|
|
||||||
r'\U0001F780-\U0001F7FF'
|
|
||||||
r'\U0001F800-\U0001F8FF'
|
|
||||||
r'\U0001F900-\U0001F9FF'
|
|
||||||
r'\U0001FA00-\U0001FA6F'
|
|
||||||
r'\U0001FA70-\U0001FAFF'
|
|
||||||
r'\u2600-\u26FF'
|
|
||||||
r'\u2700-\u27BF'
|
|
||||||
r']',
|
|
||||||
flags=re.UNICODE
|
|
||||||
)
|
|
||||||
|
|
||||||
token_pattern = re.compile(
|
|
||||||
f'({emoji_pattern.pattern})' # emoji
|
|
||||||
r'|(\w+)' # word
|
|
||||||
r'|([^\w\s])', # punctuation
|
|
||||||
flags=re.UNICODE
|
|
||||||
)
|
|
||||||
|
|
||||||
menu_background_color = arcade.color.WHITE
|
|
||||||
log_dir = 'logs'
|
|
||||||
discord_presence_id = 1393164073566208051
|
|
||||||
|
|
||||||
BLOCK_ELEMENTS = [
|
|
||||||
"html", "body", "article", "section", "nav", "aside",
|
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
|
|
||||||
"footer", "address", "p", "hr", "pre", "blockquote",
|
|
||||||
"ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
|
|
||||||
"figcaption", "main", "div", "table", "form", "fieldset",
|
|
||||||
"legend", "details", "summary"
|
|
||||||
]
|
|
||||||
|
|
||||||
SELF_CLOSING_TAGS = [
|
|
||||||
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
|
||||||
"link", "meta", "param", "source", "track", "wbr",
|
|
||||||
]
|
|
||||||
|
|
||||||
HEAD_TAGS = [
|
|
||||||
"base", "basefont", "bgsound", "noscript",
|
|
||||||
"link", "meta", "title", "style", "script",
|
|
||||||
]
|
|
||||||
|
|
||||||
INHERITED_PROPERTIES = {
|
|
||||||
"font-family": "Arial",
|
|
||||||
"font-size": "16px",
|
|
||||||
"font-style": "normal",
|
|
||||||
"font-weight": "normal",
|
|
||||||
"color": "black",
|
|
||||||
"display": "inline",
|
|
||||||
"width": "auto",
|
|
||||||
"height": "auto"
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_HEADERS = {
|
|
||||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0",
|
|
||||||
"Accept": "text/html",
|
|
||||||
"Sec-Fetch-Dest": "document",
|
|
||||||
"Sec-Fetch-Mode": "navigate",
|
|
||||||
"Sec-Fetch-Site": "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
|
|
||||||
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
|
|
||||||
big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),
|
|
||||||
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26)}
|
|
||||||
|
|
||||||
dropdown_style = {'normal': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'hover': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(49, 154, 54)),
|
|
||||||
'press': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'disabled': UIFlatButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, bg=Color(128, 128, 128))}
|
|
||||||
|
|
||||||
slider_default_style = UISliderStyle(bg=Color(128, 128, 128), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
|
|
||||||
slider_hover_style = UISliderStyle(bg=Color(49, 154, 54), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54))
|
|
||||||
|
|
||||||
slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'press': slider_hover_style, 'disabled': slider_default_style}
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
"Graphics": {
|
|
||||||
"Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"},
|
|
||||||
"Resolution": {"type": "option", "options": ["1366x768", "1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"},
|
|
||||||
"Anti-Aliasing": {"type": "option", "options": ["None", "2x MSAA", "4x MSAA", "8x MSAA", "16x MSAA"], "config_key": "anti_aliasing", "default": "4x MSAA"},
|
|
||||||
"VSync": {"type": "bool", "config_key": "vsync", "default": True},
|
|
||||||
"FPS Limit": {"type": "slider", "min": 0, "max": 480, "config_key": "fps_limit", "default": 60},
|
|
||||||
},
|
|
||||||
"Sound": {
|
|
||||||
"Music": {"type": "bool", "config_key": "music", "default": True},
|
|
||||||
"SFX": {"type": "bool", "config_key": "sfx", "default": True},
|
|
||||||
"Music Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "music_volume", "default": 50},
|
|
||||||
"SFX Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "sfx_volume", "default": 50},
|
|
||||||
},
|
|
||||||
"Miscellaneous": {
|
|
||||||
"Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True},
|
|
||||||
},
|
|
||||||
"Credits": {}
|
|
||||||
}
|
|
||||||
settings_start_category = "Graphics"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import arcade.gui, arcade
|
|
||||||
from http_client.html_parser import CSSParser
|
|
||||||
|
|
||||||
button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png"))
|
|
||||||
button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png"))
|
|
||||||
|
|
||||||
DEFAULT_STYLE_SHEET = CSSParser(open("assets/css/browser.css").read()).parse()
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import logging, traceback
|
|
||||||
|
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
import pyglet.display, arcade
|
|
||||||
|
|
||||||
def dump_platform():
|
|
||||||
import platform
|
|
||||||
logging.debug(f'Platform: {platform.platform()}')
|
|
||||||
logging.debug(f'Release: {platform.release()}')
|
|
||||||
logging.debug(f'Machine: {platform.machine()}')
|
|
||||||
logging.debug(f'Architecture: {platform.architecture()}')
|
|
||||||
|
|
||||||
def dump_gl(context=None):
|
|
||||||
if context is not None:
|
|
||||||
info = context.get_info()
|
|
||||||
else:
|
|
||||||
from pyglet.gl import gl_info as info
|
|
||||||
logging.debug(f'gl_info.get_version(): {info.get_version()}')
|
|
||||||
logging.debug(f'gl_info.get_vendor(): {info.get_vendor()}')
|
|
||||||
logging.debug(f'gl_info.get_renderer(): {info.get_renderer()}')
|
|
||||||
|
|
||||||
def print_debug_info():
|
|
||||||
logging.debug('########################## DEBUG INFO ##########################')
|
|
||||||
logging.debug('')
|
|
||||||
dump_platform()
|
|
||||||
dump_gl()
|
|
||||||
logging.debug('')
|
|
||||||
logging.debug(f'Number of screens: {len(pyglet.display.get_display().get_screens())}')
|
|
||||||
logging.debug('')
|
|
||||||
for n, screen in enumerate(pyglet.display.get_display().get_screens()):
|
|
||||||
logging.debug(f"Screen #{n+1}:")
|
|
||||||
logging.debug(f'DPI: {screen.get_dpi()}')
|
|
||||||
logging.debug(f'Scale: {screen.get_scale()}')
|
|
||||||
logging.debug(f'Size: {screen.width}, {screen.height}')
|
|
||||||
logging.debug(f'Position: {screen.x}, {screen.y}')
|
|
||||||
logging.debug('')
|
|
||||||
logging.debug('########################## DEBUG INFO ##########################')
|
|
||||||
logging.debug('')
|
|
||||||
|
|
||||||
def on_exception(*exc_info):
|
|
||||||
logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}")
|
|
||||||
|
|
||||||
def get_closest_resolution():
|
|
||||||
allowed_resolutions = [(1366, 768), (1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)]
|
|
||||||
screen_width, screen_height = arcade.get_screens()[0].width, arcade.get_screens()[0].height
|
|
||||||
if (screen_width, screen_height) in allowed_resolutions:
|
|
||||||
if not allowed_resolutions.index((screen_width, screen_height)) == 0:
|
|
||||||
closest_resolution = allowed_resolutions[allowed_resolutions.index((screen_width, screen_height))-1]
|
|
||||||
else:
|
|
||||||
closest_resolution = (screen_width, screen_height)
|
|
||||||
else:
|
|
||||||
target_width, target_height = screen_width // 2, screen_height // 2
|
|
||||||
|
|
||||||
closest_resolution = min(
|
|
||||||
allowed_resolutions,
|
|
||||||
key=lambda res: abs(res[0] - target_width) + abs(res[1] - target_height)
|
|
||||||
)
|
|
||||||
return closest_resolution
|
|
||||||
|
|
||||||
class FakePyPresence():
|
|
||||||
def __init__(self):
|
|
||||||
...
|
|
||||||
def update(self, *args, **kwargs):
|
|
||||||
...
|
|
||||||
def close(self, *args, **kwargs):
|
|
||||||
...
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def hex_to_rgb(hex_color):
|
|
||||||
hex_color = hex_color.lstrip('#')
|
|
||||||
if len(hex_color) != 6:
|
|
||||||
return (127, 127, 127)
|
|
||||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def get_color_from_name(rgb_name):
|
|
||||||
rgb_name = rgb_name.upper()
|
|
||||||
if rgb_name.startswith("LIGHT"):
|
|
||||||
color_name = rgb_name.split("LIGHT")[1]
|
|
||||||
color_value = arcade.csscolor.__dict__.get(f"LIGHT_{color_name}")
|
|
||||||
if not color_value:
|
|
||||||
color_value = arcade.color.__dict__.get(f"LIGHT_{color_name}")
|
|
||||||
|
|
||||||
if color_value:
|
|
||||||
return color_value
|
|
||||||
|
|
||||||
color_value = arcade.csscolor.__dict__.get(rgb_name)
|
|
||||||
return color_value if color_value else arcade.color.__dict__.get(rgb_name, arcade.color.GRAY)
|
|
||||||
271
uv.lock
generated
271
uv.lock
generated
@@ -1,271 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 2
|
|
||||||
requires-python = ">=3.11"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arcade"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "pyglet" },
|
|
||||||
{ name = "pymunk" },
|
|
||||||
{ name = "pytiled-parser" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/39/87eaffdfc50ec9d4b4573652ef8b80cca0592e5ccafb5fc5bc8612b1445d/arcade-3.2.0.tar.gz", hash = "sha256:1c2c56181560665f6542157b9ab316b9551274a9ee8468bae017ed5b8fee18fd", size = 41941030, upload_time = "2025-05-09T20:16:20.112Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/9a/ac86f5cbccfe5455a28308fcf2d7179af8d9c3087ad4eb45706c2a7b089b/arcade-3.2.0-py3-none-any.whl", hash = "sha256:7bb47cf643b43272e4300d8a5ca5f1b1e9e131b0f3f1d3fad013cb29528d3062", size = 42635264, upload_time = "2025-05-09T20:16:15.98Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "attrs"
|
|
||||||
version = "25.3.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload_time = "2025-03-13T11:10:22.779Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2025.7.14"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload_time = "2025-07-14T03:29:28.449Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload_time = "2025-07-14T03:29:26.863Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cffi"
|
|
||||||
version = "1.17.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pycparser" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload_time = "2024-09-04T20:43:51.124Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload_time = "2024-09-04T20:43:52.872Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload_time = "2024-09-04T20:43:56.123Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload_time = "2024-09-04T20:43:57.891Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload_time = "2024-09-04T20:44:00.18Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload_time = "2024-09-04T20:44:01.585Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload_time = "2024-09-04T20:44:03.467Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload_time = "2024-09-04T20:44:05.023Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload_time = "2024-09-04T20:44:06.444Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload_time = "2024-09-04T20:44:08.206Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload_time = "2024-09-04T20:44:09.481Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload_time = "2024-09-04T20:44:10.873Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "csd4ni3l-browser"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "arcade" },
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "pypresence" },
|
|
||||||
{ name = "ujson" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "arcade", specifier = "==3.2.0" },
|
|
||||||
{ name = "certifi", specifier = ">=2025.7.14" },
|
|
||||||
{ name = "pypresence", specifier = ">=4.3.0" },
|
|
||||||
{ name = "ujson", specifier = ">=5.10.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "11.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780, upload_time = "2024-10-15T14:24:29.672Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705, upload_time = "2024-10-15T14:22:15.419Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222, upload_time = "2024-10-15T14:22:17.681Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220, upload_time = "2024-10-15T14:22:19.826Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399, upload_time = "2024-10-15T14:22:22.129Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709, upload_time = "2024-10-15T14:22:23.953Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556, upload_time = "2024-10-15T14:22:25.706Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187, upload_time = "2024-10-15T14:22:27.362Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468, upload_time = "2024-10-15T14:22:29.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249, upload_time = "2024-10-15T14:22:31.268Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769, upload_time = "2024-10-15T14:22:32.974Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611, upload_time = "2024-10-15T14:22:35.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642, upload_time = "2024-10-15T14:22:37.736Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999, upload_time = "2024-10-15T14:22:39.654Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794, upload_time = "2024-10-15T14:22:41.598Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762, upload_time = "2024-10-15T14:22:45.952Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468, upload_time = "2024-10-15T14:22:47.789Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824, upload_time = "2024-10-15T14:22:49.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436, upload_time = "2024-10-15T14:22:51.911Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714, upload_time = "2024-10-15T14:22:53.967Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631, upload_time = "2024-10-15T14:22:56.404Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533, upload_time = "2024-10-15T14:22:58.087Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890, upload_time = "2024-10-15T14:22:59.918Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300, upload_time = "2024-10-15T14:23:01.855Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742, upload_time = "2024-10-15T14:23:03.749Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349, upload_time = "2024-10-15T14:23:06.055Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714, upload_time = "2024-10-15T14:23:07.919Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514, upload_time = "2024-10-15T14:23:10.19Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055, upload_time = "2024-10-15T14:23:12.08Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751, upload_time = "2024-10-15T14:23:13.836Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378, upload_time = "2024-10-15T14:23:15.735Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588, upload_time = "2024-10-15T14:23:17.905Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509, upload_time = "2024-10-15T14:23:19.643Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791, upload_time = "2024-10-15T14:23:21.601Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854, upload_time = "2024-10-15T14:23:23.91Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369, upload_time = "2024-10-15T14:23:27.184Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703, upload_time = "2024-10-15T14:23:28.979Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550, upload_time = "2024-10-15T14:23:30.846Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038, upload_time = "2024-10-15T14:23:32.687Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197, upload_time = "2024-10-15T14:23:35.309Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169, upload_time = "2024-10-15T14:23:37.33Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828, upload_time = "2024-10-15T14:23:39.826Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycparser"
|
|
||||||
version = "2.22"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload_time = "2024-03-30T13:22:22.564Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload_time = "2024-03-30T13:22:20.476Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyglet"
|
|
||||||
version = "2.1.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/bc/0533ccb30566ee59b540d700dbbf916dafa89132a4d582d0fd1fe158243d/pyglet-2.1.6.tar.gz", hash = "sha256:18483880b1411b39692eaf7756819285797b1aaf9ef63d40eb9f9b5d01c63416", size = 6546705, upload_time = "2025-04-27T01:12:30.995Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/e16f9b56c4a935934341e385753d0d0a2a83b7d320e52906b44f32698feb/pyglet-2.1.6-py3-none-any.whl", hash = "sha256:52ef9e75f3969b6a28bfa5c223e50ff03a05c2baa67bfe00d2a9eec4e831a7c5", size = 983998, upload_time = "2025-04-27T01:12:26.307Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pymunk"
|
|
||||||
version = "6.9.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "cffi" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/08/1513c868bc2a6bfa22d47acded27f5525c1db10bf1db4fdfa39160991616/pymunk-6.9.0.tar.gz", hash = "sha256:765f7c561a859a1b565bc517a47cc3992d6258e860f9174c533033c218af63c3", size = 3104088, upload_time = "2024-10-13T09:02:40.008Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/ba/34524aac6c57990aa9561c4a949543794e5f7128a0b01537ed061bdaed08/pymunk-6.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:536cf3ef9a3add0ea04d83a4c01fe090ff137fb591c3b6fff6e69102384ec5d5", size = 364338, upload_time = "2024-10-13T08:58:08.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/9a/0d4931e3114495c31b600a17f27d5541f2ee35883e7c693199e1ccdf1ab0/pymunk-6.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e474bb748ded01d96d6eac8e282446baef324b67e0280213b495b1f936c06e7", size = 346937, upload_time = "2024-10-13T08:58:10.604Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/d0/acd6a6cd8266ac0333792ac3ae36558a58859ca806e0add8f5ea01627b24/pymunk-6.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f54bd14512ca5fed0e77f964b1de4e7da1a31386dbf125e33482874d69bb6537", size = 1065273, upload_time = "2024-10-13T08:58:13.012Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/d3/2e5763d2eea69e8953782da83fe81a0235650339c22a4f8c65ecdd07cec0/pymunk-6.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc8d6fe79f77f3ed6e2f33682d355eedb6864684120b845a3501fdf2d3efdcb6", size = 988611, upload_time = "2024-10-13T08:58:15.262Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/db/ff2cfa5b87d3e60992b2264a03ffedc738de64d0107b4ce96c623f9098e7/pymunk-6.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:70ee413899672c2d7d2ffbecabee133dba49a109867b520d77c829c0d9b3fe92", size = 974971, upload_time = "2024-10-13T08:58:17.706Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/44/8fd8677048aa864d91915702522c70c5aaadedfd7cd95000b75d7aabeffd/pymunk-6.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d163dcba2e5814bc5f1274e0ee6ec2a7e06bed8bf0050f30f22b604634bf7dbc", size = 1037097, upload_time = "2024-10-13T08:58:20.264Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/fc/e6b8bf53255f2012dbdf4a2b063b6c02f8c13ce13b21fdfd84dda64fea80/pymunk-6.9.0-cp311-cp311-win32.whl", hash = "sha256:5d3ae7df3d39afe5b11633496cd464b198d5c62bec69f767f3b61f9fe7f09b98", size = 315321, upload_time = "2024-10-13T08:58:22.475Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/3c/925a0193bbcca7203f46fc531f4f0703885c102c1e2c118c8db35816aee3/pymunk-6.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:2db4797ecec3668d51bc112a37192ee1836e236bbacdf5ed12f5a994cf1bae33", size = 366711, upload_time = "2024-10-13T08:58:24.796Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/96/d8505f4e9661c0e5343db5492895b90b2ada6ec4547fdc7a2df50eb0cdf2/pymunk-6.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02bb0fbbbce2b12c18a033e2cec747e6c4b0db93d2cb9a20f45e569b571ba184", size = 364703, upload_time = "2024-10-13T08:58:27.144Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/3e/610a2f2b0c6c14038168f6f862148cb245aef867b01906ce18704acafe1c/pymunk-6.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6aae4f93ac686d5e2ec60b01faa1b3722a8ab630464d0c127e16462e7bef6292", size = 347056, upload_time = "2024-10-13T08:58:29.39Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/dd/4e12fb3671a6c4f2c0604420f0f15b5402b05c4964bba001088a3d92e3b9/pymunk-6.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7734d13e490e84665b1f03e616270b248d5279ed34e03859267f67868f1b94c", size = 1071014, upload_time = "2024-10-13T08:58:32.274Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/f8/0618a9204aff896da8b2a9df44179390b178bf00b189851affd4809b1f03/pymunk-6.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b05dbfa58d366dea860f7259ca48483922a83620ab6a19effaa74e85a4251966", size = 990358, upload_time = "2024-10-13T08:58:35.295Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/67/ea2ff4a26b66acad394e4f28e4e316fbe306d34909eca401baae211ca182/pymunk-6.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cb9520c52c043de4b2b1f83979f0d097929f6ff13c8a4059d9d211b98ae25887", size = 976300, upload_time = "2024-10-13T08:58:37.339Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/d9/a69b268712dceacf227cfff74401e2292b53050383661d456605a1928a84/pymunk-6.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:da0e153d321073cd07a48380cfc1b7bd8d40bf4ee1b14a7ede33d90a69ee0452", size = 1042511, upload_time = "2024-10-13T08:58:40.044Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/40/21c2a08b027d99f351b75daa36f8a2e2385daba45098078d225811275ff8/pymunk-6.9.0-cp312-cp312-win32.whl", hash = "sha256:8325c9092345764876b1c3855126cb14450dc83dc5b141ff54983a7c77fbae52", size = 315339, upload_time = "2024-10-13T09:01:37.995Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/b4/0a18c632f96924f969924cc5903689afcaf474d4c472305805dab391b247/pymunk-6.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:13246a79b599c44d174f5619596c62b656d8539797f28bdb2797c4b700c90a33", size = 366671, upload_time = "2024-10-13T09:01:39.965Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/5a/c76904d21f3fdb0b713b3a8056622733a0b773f7e55ef974fa4546068cbd/pymunk-6.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c59e5cf904e148dd0d35cffb7bafe146835042de9280672cafecc3a41caf7a3", size = 364703, upload_time = "2024-10-13T09:01:42.628Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/b2/378d54b79812da5312b10de272c27aa0ac621498e059aa50eb4eec33ab52/pymunk-6.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4cbc2d37f69d85fedc1097af64edc8f4c43973a13429d51004883cbb9342875e", size = 347058, upload_time = "2024-10-13T09:01:44.529Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/a8/c7ea141a1d0e3f5b08ad653f0b5a4ebc0e5854f92bc7049a2a921fbe0d65/pymunk-6.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd64ef76e9e47fda929a2961fe98759ac46b5a7b6126d1ba3e6f04493da6519b", size = 1070851, upload_time = "2024-10-13T09:01:46.638Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/a2/f40bcc9be90c2af1fe8cf4ba4281385b48d9f5667f03f6834c49aba600fd/pymunk-6.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c568c7402acd5d9a55e3965565ae0a596e4603ba8a7b7b7f0952efadd0e69524", size = 990371, upload_time = "2024-10-13T09:01:49.622Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/ae/ff7fdf1c8d32ba89d1ccada39b5f7ed66e35420b8d31bdc9af6d5d20ea2f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f6cbe3d06e468be11a615d4facecc4a870bf58c1a27c365e655b5a85685ec942", size = 976294, upload_time = "2024-10-13T09:01:52.196Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/90/64ef000011f0c930b42354f0d91a07b4bc7f70819ec5b6034b84198bf53f/pymunk-6.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65a9a93a51dbaf1c77efa4d2425549888a1eda9f5c9cd9a5a89b7ca66310968a", size = 1042493, upload_time = "2024-10-13T09:01:55.665Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/fb/6516bd5fe565ea51a88308869632dfc896ca6b05b2579b016ffa8047a8ec/pymunk-6.9.0-cp313-cp313-win32.whl", hash = "sha256:a78b37bb360e715657c76caedaf40cdaaf6dab354d497eda481a976cc5cab3d7", size = 315341, upload_time = "2024-10-13T09:01:58.049Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/7c/1542df7ffbff70a4523ccb02c9241c9fe4dc24c77b747e2c16fb94891156/pymunk-6.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d6419e1531df80ff0bb6f1f8215e044f57415514386b7b212dc148919ca629ed", size = 366673, upload_time = "2024-10-13T09:01:59.733Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pypresence"
|
|
||||||
version = "4.3.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/2e/d110f862720b5e3ba1b0b719657385fc4151929befa2c6981f48360aa480/pypresence-4.3.0.tar.gz", hash = "sha256:a6191a3af33a9667f2a4ef0185577c86b962ee70aa82643c472768a6fed1fbf3", size = 10696, upload_time = "2023-07-08T00:33:53.49Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/40/1d30b30e18f81eb71365681223971a9822a89b3d6ee5269dd2aa955bc228/pypresence-4.3.0-py2.py3-none-any.whl", hash = "sha256:af878c6d49315084f1b108aec86b31915080614d9421d6dd3a44737aba9ff13f", size = 11778, upload_time = "2023-07-08T00:33:52.018Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytiled-parser"
|
|
||||||
version = "2.2.9"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "attrs" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/79/62/0d8a2220ee0747522f3b73e4f38bea7c78aefdf707afb86decf26f799fc5/pytiled_parser-2.2.9.tar.gz", hash = "sha256:225269fdd37afcbcd3b76ea3e2cab6b1e742387027106055990db43fd7451ebd", size = 45958, upload_time = "2025-01-23T18:43:30.538Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/f7/6b6c51b50ed8681a31146e5e7ac325b78fe776ff48b1ec8f56d7e4995d72/pytiled_parser-2.2.9-py2.py3-none-any.whl", hash = "sha256:37f73d31950bf4d02ee3bda59f3d6123c55194dc8d8e876821dd2080af5f1f91", size = 44452, upload_time = "2025-01-23T18:43:28.207Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-extensions"
|
|
||||||
version = "4.14.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload_time = "2025-07-04T13:28:34.16Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload_time = "2025-07-04T13:28:32.743Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ujson"
|
|
||||||
version = "5.10.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885, upload_time = "2024-05-14T02:02:34.233Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/ec/3c551ecfe048bcb3948725251fb0214b5844a12aa60bee08d78315bb1c39/ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00", size = 55353, upload_time = "2024-05-14T02:00:48.04Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/9f/4731ef0671a0653e9f5ba18db7c4596d8ecbf80c7922dd5fe4150f1aea76/ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126", size = 51813, upload_time = "2024-05-14T02:00:49.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/2b/44d6b9c1688330bf011f9abfdb08911a9dc74f76926dde74e718d87600da/ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8", size = 51988, upload_time = "2024-05-14T02:00:50.484Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/45/f5f5667427c1ec3383478092a414063ddd0dfbebbcc533538fe37068a0a3/ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b", size = 53561, upload_time = "2024-05-14T02:00:52.146Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/21/a0c265cda4dd225ec1be595f844661732c13560ad06378760036fc622587/ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9", size = 58497, upload_time = "2024-05-14T02:00:53.366Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/36/8fde862094fd2342ccc427a6a8584fed294055fdee341661c78660f7aef3/ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f", size = 997877, upload_time = "2024-05-14T02:00:55.095Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/37/9208e40d53baa6da9b6a1c719e0670c3f474c8fc7cc2f1e939ec21c1bc93/ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4", size = 1140632, upload_time = "2024-05-14T02:00:57.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/d5/2626c87c59802863d44d19e35ad16b7e658e4ac190b0dead17ff25460b4c/ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1", size = 1043513, upload_time = "2024-05-14T02:00:58.488Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/ee/03662ce9b3f16855770f0d70f10f0978ba6210805aa310c4eebe66d36476/ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f", size = 38616, upload_time = "2024-05-14T02:01:00.463Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/20/952dbed5895835ea0b82e81a7be4ebb83f93b079d4d1ead93fcddb3075af/ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720", size = 42071, upload_time = "2024-05-14T02:01:02.211Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642, upload_time = "2024-05-14T02:01:04.055Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807, upload_time = "2024-05-14T02:01:05.25Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972, upload_time = "2024-05-14T02:01:06.458Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686, upload_time = "2024-05-14T02:01:07.618Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591, upload_time = "2024-05-14T02:01:08.901Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853, upload_time = "2024-05-14T02:01:10.772Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689, upload_time = "2024-05-14T02:01:12.214Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576, upload_time = "2024-05-14T02:01:14.39Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764, upload_time = "2024-05-14T02:01:15.83Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211, upload_time = "2024-05-14T02:01:17.567Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646, upload_time = "2024-05-14T02:01:19.26Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806, upload_time = "2024-05-14T02:01:20.593Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975, upload_time = "2024-05-14T02:01:21.904Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693, upload_time = "2024-05-14T02:01:23.742Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594, upload_time = "2024-05-14T02:01:25.554Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853, upload_time = "2024-05-14T02:01:27.151Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694, upload_time = "2024-05-14T02:01:29.113Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580, upload_time = "2024-05-14T02:01:31.447Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766, upload_time = "2024-05-14T02:01:32.856Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212, upload_time = "2024-05-14T02:01:33.97Z" },
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user