Merge pull request #388 from arabcoders/dev
Some checks failed
Build Native wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, windows-latest) (push) Has been cancelled
Some checks failed
Build Native wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, windows-latest) (push) Has been cancelled
Remove webview in favor of using built-in browser.
This commit is contained in:
commit
c91ca38035
16 changed files with 644 additions and 453 deletions
18
.github/workflows/native-build.yml
vendored
18
.github/workflows/native-build.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Build WebView wrappers
|
||||
name: Build Native wrappers
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
@ -52,10 +52,6 @@ jobs:
|
|||
restore-keys: |
|
||||
choco-${{ runner.os }}-${{ matrix.arch }}-
|
||||
|
||||
- name: Install Qt (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: choco install -y qt6-base-dev
|
||||
|
||||
- name: Cache Python venv
|
||||
id: cache-python
|
||||
uses: actions/cache@v4
|
||||
|
|
@ -77,16 +73,6 @@ jobs:
|
|||
uv venv --system-site-packages --relocatable
|
||||
uv sync --link-mode=copy --active
|
||||
|
||||
- name: Install PyInstaller + Qt backend (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
uv pip install pyinstaller pywebview QtPy PySide6
|
||||
|
||||
- name: Install PyInstaller + Qt backend (Other OSs)
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: |
|
||||
uv pip install pyinstaller pywebview[qt]
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
|
|
@ -107,7 +93,7 @@ jobs:
|
|||
|
||||
- name: Build native binary
|
||||
run: |
|
||||
uv run pyinstaller ./app.spec
|
||||
uv tool run pyinstaller ./app.spec
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
|
|||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
|
@ -24,6 +24,7 @@
|
|||
"bilibili",
|
||||
"brainicism",
|
||||
"brotlicffi",
|
||||
"choco",
|
||||
"consoletitle",
|
||||
"continuedl",
|
||||
"cookiesfrombrowser",
|
||||
|
|
@ -41,6 +42,7 @@
|
|||
"engineio",
|
||||
"Errno",
|
||||
"euuo",
|
||||
"excepthook",
|
||||
"faststart",
|
||||
"finaldir",
|
||||
"flac",
|
||||
|
|
@ -91,10 +93,6 @@
|
|||
"pycryptodome",
|
||||
"pyinstaller",
|
||||
"pypi",
|
||||
"pyside",
|
||||
"pywebview",
|
||||
"qstr",
|
||||
"qtpy",
|
||||
"quicktime",
|
||||
"rejecttitle",
|
||||
"remux",
|
||||
|
|
@ -103,6 +101,7 @@
|
|||
"SIGUSR",
|
||||
"smhd",
|
||||
"socketio",
|
||||
"softprops",
|
||||
"sstr",
|
||||
"SUPPRESSHELP",
|
||||
"tebibytes",
|
||||
|
|
|
|||
23
app.spec
23
app.spec
|
|
@ -1,25 +1,10 @@
|
|||
import os
|
||||
import platform
|
||||
import sys
|
||||
import tomllib
|
||||
|
||||
block_cipher = None
|
||||
|
||||
machine = platform.machine().lower()
|
||||
binaries = []
|
||||
if sys.platform.startswith("linux"):
|
||||
if machine in ("x86_64", "amd64"):
|
||||
libdir = "/usr/lib/x86_64-linux-gnu"
|
||||
elif machine in ("aarch64", "arm64"):
|
||||
libdir = "/usr/lib/aarch64-linux-gnu"
|
||||
else:
|
||||
libdir = "/usr/lib"
|
||||
binaries = []
|
||||
elif sys.platform == "darwin":
|
||||
binaries = []
|
||||
elif sys.platform.startswith("win"):
|
||||
# Windows DLLs
|
||||
binaries = []
|
||||
|
||||
with open("./uv.lock", "rb") as f:
|
||||
lock = tomllib.load(f)
|
||||
|
|
@ -37,9 +22,9 @@ hidden = [
|
|||
hidden = [f.replace("-", "_") for f in hidden]
|
||||
|
||||
a = Analysis( # noqa: F821 # type: ignore
|
||||
["app/native.py"],
|
||||
["app/local.py"],
|
||||
pathex=[os.getcwd()],
|
||||
binaries=binaries,
|
||||
binaries=[],
|
||||
datas=[
|
||||
("ui/exported", "ui/exported"),
|
||||
("app/", "app/"),
|
||||
|
|
@ -47,7 +32,7 @@ a = Analysis( # noqa: F821 # type: ignore
|
|||
hiddenimports=hidden,
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
excludes=["libstdc++.so.6"],
|
||||
cipher=block_cipher,
|
||||
)
|
||||
|
||||
|
|
@ -63,7 +48,7 @@ exe = EXE( # type: ignore # noqa: F821
|
|||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False, # Turn on to True if you want a console window for debugging.
|
||||
console=True,
|
||||
icon="ui/public/favicon.ico",
|
||||
onefile=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class Config:
|
|||
"""Enable yt-dlp debugging."""
|
||||
|
||||
is_native: bool = False
|
||||
"Is the application running in webview."
|
||||
"Is the application running in natively."
|
||||
|
||||
prevent_live_premiere: bool = False
|
||||
"""Prevent downloading of the initial premiere live broadcast."""
|
||||
|
|
@ -288,7 +288,9 @@ class Config:
|
|||
@staticmethod
|
||||
def get_instance(is_native: bool = False) -> "Config":
|
||||
"""Static access method."""
|
||||
return Config(is_native) if not Config.__instance else Config.__instance
|
||||
cls: Config = Config(is_native) if not Config.__instance else Config.__instance
|
||||
cls.is_native = is_native or cls.is_native
|
||||
return cls
|
||||
|
||||
@staticmethod
|
||||
def get_manager() -> SyncManager:
|
||||
|
|
|
|||
133
app/local.py
Normal file
133
app/local.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env python3
|
||||
# flake8: noqa: SIM115 S310 T201
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
sys.path.insert(0, os.path.join(getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))), "app"))
|
||||
|
||||
APP_NAME = "YTPTube"
|
||||
APP_ROOT = str((Path(__file__).parent / "..").resolve())
|
||||
if APP_ROOT not in sys.path:
|
||||
sys.path.insert(0, APP_ROOT)
|
||||
|
||||
import platformdirs # type: ignore
|
||||
|
||||
|
||||
def set_env():
|
||||
dct = {}
|
||||
|
||||
if not os.getenv("YTP_CONFIG_PATH"):
|
||||
dct["YTP_CONFIG_PATH"] = platformdirs.user_config_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True)
|
||||
|
||||
if not os.getenv("YTP_TEMP_PATH"):
|
||||
dct["YTP_TEMP_PATH"] = platformdirs.user_cache_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True)
|
||||
|
||||
if not os.getenv("YTP_DOWNLOAD_PATH"):
|
||||
dct["YTP_DOWNLOAD_PATH"] = platformdirs.user_downloads_dir()
|
||||
|
||||
if os.getenv("YTP_ACCESS_LOG", None) is None:
|
||||
dct["YTP_ACCESS_LOG"] = "false"
|
||||
|
||||
if not os.getenv("YTP_HOST"):
|
||||
dct["YTP_HOST"] = "127.0.0.1"
|
||||
|
||||
if dct:
|
||||
os.environ.update(dct)
|
||||
|
||||
|
||||
def open_browser_when_ready(url: str, timeout: float = 5.0) -> None:
|
||||
import threading
|
||||
|
||||
def wait_then_open():
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=1) as response:
|
||||
if 200 == response.status:
|
||||
os.environ.pop("LD_LIBRARY_PATH", None)
|
||||
webbrowser.open_new(url)
|
||||
return
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
|
||||
print(f"Failed to open browser automatically within {timeout} seconds. Please open {url} manually.")
|
||||
|
||||
threading.Thread(target=wait_then_open, daemon=True).start()
|
||||
|
||||
|
||||
def app_start(host: str, port: int) -> None:
|
||||
import asyncio
|
||||
|
||||
from app.main import Main
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
Main(is_native=True).start(host, port)
|
||||
|
||||
|
||||
def update_env_file(env_file: Path, port: int) -> None:
|
||||
lines = []
|
||||
if env_file.exists():
|
||||
with env_file.open("r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
port_line = f"YTP_PORT={port}\n"
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("YTP_PORT="):
|
||||
lines[i] = port_line
|
||||
break
|
||||
else:
|
||||
lines.append(port_line)
|
||||
|
||||
with env_file.open("w", encoding="utf-8") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Start YTPTube server")
|
||||
parser.add_argument("--no-browser", action="store_true", help="Do not open browser on start")
|
||||
args = parser.parse_args()
|
||||
|
||||
set_env()
|
||||
|
||||
env_file: Path = Path(os.getenv("YTP_CONFIG_PATH")) / ".env"
|
||||
|
||||
port = None
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
port = os.getenv("YTP_PORT")
|
||||
|
||||
host = os.getenv("YTP_HOST", "127.0.0.1")
|
||||
|
||||
if not port or 8081 == int(port):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind((host, 0))
|
||||
port = s.getsockname()[1]
|
||||
update_env_file(env_file, port)
|
||||
|
||||
url = f"http://{host}:{port}"
|
||||
|
||||
env_no_browser: bool = os.getenv("YTP_NO_BROWSER", "false").lower() in ("1", "true", "yes")
|
||||
if not args.no_browser and not env_no_browser:
|
||||
open_browser_when_ready(url)
|
||||
|
||||
app_start(host, int(port))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -141,6 +141,8 @@ class Main:
|
|||
def started(_):
|
||||
LOG.info("=" * 40)
|
||||
LOG.info(f"YTPTube {self._config.app_version} - started on http://{host}:{port}{self._config.base_path}")
|
||||
if self._config.is_native:
|
||||
LOG.info("Running in native mode.")
|
||||
LOG.info("=" * 40)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
|
|||
187
app/native.py
187
app/native.py
|
|
@ -1,187 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import platformdirs
|
||||
|
||||
sys.path.insert(0, os.path.join(getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))), "app"))
|
||||
|
||||
APP_NAME = "YTPTube"
|
||||
APP_ROOT = str((Path(__file__).parent / "..").resolve())
|
||||
if APP_ROOT not in sys.path:
|
||||
sys.path.insert(0, APP_ROOT)
|
||||
|
||||
try:
|
||||
import webview # type: ignore
|
||||
|
||||
if "linux" in sys.platform:
|
||||
os.environ.setdefault("LC_ALL", "C.UTF-8")
|
||||
os.environ.setdefault("LANG", "C.UTF-8")
|
||||
import webview.platforms.qt # type: ignore
|
||||
|
||||
# monkey patch the download handler for pywebview to support qt6.
|
||||
def on_download_requested(self, download):
|
||||
from qtpy import QtCore # type: ignore
|
||||
from qtpy.QtWidgets import QFileDialog # type: ignore
|
||||
|
||||
old_path = download.url().path()
|
||||
suffix = QtCore.QFileInfo(old_path).suffix()
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self, self.localization["global.saveFile"], old_path, "*." + suffix
|
||||
)
|
||||
if filename:
|
||||
if hasattr(download, "setPath"):
|
||||
download.setPath(filename)
|
||||
else:
|
||||
download.setDownloadDirectory(os.path.dirname(filename))
|
||||
download.setDownloadFileName(os.path.basename(filename))
|
||||
download.accept()
|
||||
else:
|
||||
download.cancel()
|
||||
|
||||
webview.platforms.qt.BrowserView.on_download_requested = on_download_requested
|
||||
|
||||
except ImportError as e:
|
||||
pkgs = "pywebview[edgechromium]" if os.name == "nt" else "pywebview[qt]"
|
||||
msg: str = f"Please run 'uv pip install {pkgs}' to run YTPTube in native mode."
|
||||
raise ImportError(msg) from e
|
||||
|
||||
|
||||
def error_window(exc: Exception | str) -> None:
|
||||
trace: str = "\n".join(traceback.format_exception(exc)) if isinstance(exc, Exception) else exc
|
||||
webview.create_window(
|
||||
f"{APP_NAME} - Error",
|
||||
html=f"<h1 style='color:red;'>An error occurred</h1><pre>{trace}</pre>",
|
||||
width=600,
|
||||
height=400,
|
||||
resizable=True,
|
||||
)
|
||||
webview.start(
|
||||
gui="edgechromium" if os.name == "nt" else "qt",
|
||||
debug=False,
|
||||
storage_path=str(Path(os.getenv("YTP_TEMP_PATH", os.getcwd())) / "webview"),
|
||||
private_mode=False,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def set_env():
|
||||
defaults = {
|
||||
"YTP_CONFIG_PATH": lambda: platformdirs.user_config_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True),
|
||||
"YTP_TEMP_PATH": lambda: platformdirs.user_cache_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True),
|
||||
"YTP_DOWNLOAD_PATH": lambda: platformdirs.user_downloads_dir(),
|
||||
"YTP_ACCESS_LOG": "false",
|
||||
"YTP_BROWSER_ENABLED": "true",
|
||||
"YTP_BROWSER_CONTROL_ENABLED": "true",
|
||||
}
|
||||
|
||||
for key, value in defaults.items():
|
||||
if os.getenv(key) is not None:
|
||||
continue
|
||||
|
||||
os.environ[key] = value() if callable(value) else value
|
||||
|
||||
|
||||
def run_backend(host: str, port: int, ready_event: threading.Event, error_queue: queue.Queue):
|
||||
try:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
from app.main import Main
|
||||
|
||||
Main(is_native=True).start(host, port, cb=lambda: ready_event.set())
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
error_queue.put(e)
|
||||
ready_event.set()
|
||||
|
||||
|
||||
def get_usable_port(host: str = "127.0.0.1") -> int:
|
||||
"""
|
||||
Get a usable port on the specified host.
|
||||
If no port is available, it will raise an OSError.
|
||||
"""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind((host, 0))
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
|
||||
return port
|
||||
|
||||
|
||||
def main():
|
||||
host = "127.0.0.1"
|
||||
set_env()
|
||||
|
||||
cfg_path: Path = Path(os.getenv("YTP_CONFIG_PATH")) / "webview.json"
|
||||
win_conf: dict[str, int] = {}
|
||||
|
||||
if cfg_path.exists():
|
||||
data = json.loads(cfg_path.read_text())
|
||||
for key in ("width", "height", "x", "y"):
|
||||
if key in data:
|
||||
win_conf[key] = data[key]
|
||||
|
||||
port = get_usable_port(host)
|
||||
|
||||
ready = threading.Event()
|
||||
errors: queue.Queue = queue.Queue()
|
||||
threading.Thread(target=run_backend, args=(host, port, ready, errors), daemon=False).start()
|
||||
|
||||
ready.wait(timeout=5)
|
||||
|
||||
if not errors.empty():
|
||||
error_window(errors.get())
|
||||
return
|
||||
|
||||
from app.library.version import APP_VERSION
|
||||
|
||||
gui = "edgechromium" if os.name == "nt" else "qt"
|
||||
create_kwargs = {**win_conf, "resizable": True}
|
||||
window = webview.create_window(f"{APP_NAME} - {APP_VERSION}", f"http://{host}:{port}", **create_kwargs)
|
||||
|
||||
def save_geometry():
|
||||
cfg = {
|
||||
"port": port,
|
||||
"width": window.width,
|
||||
"height": window.height,
|
||||
"x": window.x,
|
||||
"y": window.y,
|
||||
}
|
||||
cfg_path.write_text(json.dumps(cfg))
|
||||
|
||||
window.events.resized += lambda *_: save_geometry()
|
||||
window.events.moved += lambda *_: save_geometry()
|
||||
window.events.closing += lambda *_: os._exit(0)
|
||||
|
||||
webview.settings["ALLOW_DOWNLOADS"] = True
|
||||
webview.settings["OPEN_DEVTOOLS_IN_DEBUG"] = False
|
||||
webview.start(
|
||||
gui=gui,
|
||||
debug=True,
|
||||
storage_path=str(Path(os.getenv("YTP_TEMP_PATH", os.getcwd())) / "webview"),
|
||||
private_mode=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
freeze_support()
|
||||
main()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
error_window(e)
|
||||
os._exit(1)
|
||||
59
app/routes/api/system.py
Normal file
59
app/routes/api/system.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web import Request, Response
|
||||
from aiohttp.web_runner import GracefulExit
|
||||
|
||||
from app.library.config import Config
|
||||
from app.library.encoder import Encoder
|
||||
from app.library.Events import EventBus, Events
|
||||
from app.library.router import route
|
||||
|
||||
LOG: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@route("POST", "api/system/shutdown", "system.shutdown")
|
||||
async def shutdown_system(request: Request, config: Config, encoder: Encoder, notify: EventBus) -> Response:
|
||||
"""
|
||||
Get the presets.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
config (Config): The config instance.
|
||||
encoder (Encoder): The encoder instance.
|
||||
notify (EventBus): The event bus instance.
|
||||
|
||||
Returns:
|
||||
Response: The response object.
|
||||
|
||||
"""
|
||||
if config.is_native is not True:
|
||||
return web.json_response(
|
||||
{"error": "Shutdown is only available in the native mode."},
|
||||
status=web.HTTPBadRequest.status_code,
|
||||
)
|
||||
|
||||
app = request.app
|
||||
|
||||
async def do_shutdown():
|
||||
await notify.emit(
|
||||
Events.SHUTDOWN,
|
||||
data={"app": app},
|
||||
title="Application Shutdown",
|
||||
message="Shutdown initiated by user request.",
|
||||
)
|
||||
await asyncio.sleep(0.5)
|
||||
await app.shutdown()
|
||||
await app.cleanup()
|
||||
raise GracefulExit
|
||||
|
||||
# Schedule shutdown after response
|
||||
asyncio.create_task(do_shutdown())
|
||||
return web.json_response(
|
||||
data={
|
||||
"message": "The application shutting down.",
|
||||
},
|
||||
status=web.HTTPOk.status_code,
|
||||
dumps=encoder.encode,
|
||||
)
|
||||
|
|
@ -78,12 +78,7 @@ indent-width = 4
|
|||
target-version = "py311"
|
||||
|
||||
[project.optional-dependencies]
|
||||
webview = [
|
||||
#windows only
|
||||
"qtpy; sys_platform == 'win32'",
|
||||
"pyside6; sys_platform == 'win32'",
|
||||
# all
|
||||
"pywebview",
|
||||
installer = [
|
||||
"pyinstaller",
|
||||
]
|
||||
|
||||
|
|
|
|||
151
ui/app/components/Dialog.vue
Normal file
151
ui/app/components/Dialog.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<style scoped>
|
||||
/* container fades */
|
||||
.dialog-enter-active,
|
||||
.dialog-leave-active {
|
||||
transition: opacity .18s ease;
|
||||
}
|
||||
|
||||
.dialog-enter-from,
|
||||
.dialog-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* animate the card itself */
|
||||
.dialog-enter-active .modal-card,
|
||||
.dialog-leave-active .modal-card {
|
||||
transition: transform .18s ease, opacity .18s ease;
|
||||
}
|
||||
|
||||
.dialog-enter-from .modal-card {
|
||||
transform: translateY(-8px);
|
||||
opacity: .98;
|
||||
}
|
||||
|
||||
.dialog-leave-to .modal-card {
|
||||
transform: translateY(-8px);
|
||||
opacity: .98;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<transition name="dialog" @after-enter="focusInput">
|
||||
<div id="app-dialog-host" v-if="state.current" class="modal is-active" @keydown.esc="onCancel">
|
||||
<div class="modal-background" @click="onCancel" />
|
||||
<div class="modal-card" @keydown.enter.stop.prevent="onEnter">
|
||||
<header class="modal-card-head p-4">
|
||||
<p class="modal-card-title">{{ state.current?.opts.title ?? defaultTitle }}</p>
|
||||
<button class="delete" aria-label="close" @click="onCancel" />
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p v-if="state.current?.opts.message" class="mb-3">
|
||||
{{ state.current?.opts.message }}
|
||||
</p>
|
||||
|
||||
<!-- prompt input -->
|
||||
<div v-if="state.current?.type === 'prompt'" class="field">
|
||||
<div class="control">
|
||||
<input ref="inputEl" class="input" type="text" v-model="localInput"
|
||||
:placeholder="(state.current?.opts as any)?.placeholder ?? ''" @keyup.stop />
|
||||
</div>
|
||||
<p v-if="state.errorMsg" class="help is-danger is-bold is-unselectable">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-exclamation-triangle" /></span>
|
||||
<span>{{ state.errorMsg }}</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot p-4 is-justify-content-flex-end">
|
||||
<template v-if="state.current?.type === 'alert'">
|
||||
<button class="button is-danger" @click="onEnter">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-check" /></span>
|
||||
<span>{{ (state.current?.opts as any)?.confirmText ?? 'OK' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="state.current?.type === 'confirm' || state.current?.type === 'prompt'">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-primary" @click="onEnter">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-check" /></span>
|
||||
<span>{{ (state.current?.opts as any)?.confirmText ?? 'OK' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-info" @click="onCancel">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-times" /></span>
|
||||
<span>{{ (state.current?.opts as any)?.cancelText ?? 'Cancel' }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed } from 'vue'
|
||||
import { useDialog } from '~/composables/useDialog'
|
||||
|
||||
const { state, confirm, cancel } = useDialog()
|
||||
|
||||
const localInput = ref('')
|
||||
|
||||
watch(() => state.current, (cur) => {
|
||||
if (state.current) {
|
||||
disableOpacity()
|
||||
}
|
||||
else {
|
||||
enableOpacity()
|
||||
}
|
||||
|
||||
localInput.value = cur?.type === 'prompt' ? (cur.opts as any).initial ?? '' : ''
|
||||
}, { immediate: true })
|
||||
|
||||
const inputEl = ref<HTMLInputElement>()
|
||||
const focusPrimary = () => {
|
||||
const root = document.getElementById('app-dialog-host')
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
const btn = root.querySelector<HTMLButtonElement>('.modal-card-foot .button.is-primary')
|
||||
btn?.focus()
|
||||
}
|
||||
const focusInput = async () => {
|
||||
await nextTick()
|
||||
if (state.current?.type === 'prompt') {
|
||||
requestAnimationFrame(() => inputEl.value?.focus({ preventScroll: true }))
|
||||
return
|
||||
}
|
||||
requestAnimationFrame(focusPrimary)
|
||||
}
|
||||
|
||||
const onCancel = () => cancel()
|
||||
const onEnter = () => confirm(localInput.value)
|
||||
|
||||
const defaultTitle = computed(() => {
|
||||
if (!state.current) {
|
||||
return ''
|
||||
}
|
||||
switch (state.current.type) {
|
||||
case 'alert':
|
||||
return 'Alert'
|
||||
case 'confirm':
|
||||
return 'Confirm'
|
||||
case 'prompt':
|
||||
return 'Input required'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="control" style="width:100%;">
|
||||
<input v-model="model" @focus="onFocus" @blur="hideList" @keydown="handleKeydown" @input="onInput" class="input"
|
||||
:placeholder="placeholder" autocomplete="new-password" style="width:100%; position:relative; z-index:2;"
|
||||
:disabled="disabled" />
|
||||
:disabled="disabled" :id="id" />
|
||||
</div>
|
||||
<div class="dropdown-menu" role="menu" style="width:100%; z-index:3;">
|
||||
<div class="dropdown-content" style="width:100%; max-height:10em; overflow-y:auto;">
|
||||
|
|
@ -27,7 +27,8 @@ import type { AutoCompleteOptions } from '~/types/autocomplete'
|
|||
const props = defineProps<{
|
||||
options: AutoCompleteOptions
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
disabled?: boolean,
|
||||
id?: string
|
||||
}>()
|
||||
|
||||
const model = defineModel<string>()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="dropdown" :class="{ 'is-active': showList && filteredOptions.length }" style="width:100%;">
|
||||
<div class="control" style="width:100%;">
|
||||
<textarea v-model="localValue" @input="onInput" @focus="showList = true" @blur="hideList" @keydown="onKeydown"
|
||||
<textarea :id="id" v-model="localValue" @input="onInput" @focus="showList = true" @blur="hideList" @keydown="onKeydown"
|
||||
class="textarea" :placeholder="placeholder" autocomplete="off" style="width:100%; position:relative; z-index:2;"
|
||||
rows="4" :disabled="disabled" />
|
||||
</div>
|
||||
|
|
@ -26,6 +26,7 @@ const props = defineProps<{
|
|||
options: AutoCompleteOptions
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
}>()
|
||||
|
||||
const model = defineModel<string>()
|
||||
|
|
|
|||
91
ui/app/components/shutdown.vue
Normal file
91
ui/app/components/shutdown.vue
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div id="main_container" class="container">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<section
|
||||
class="hero has-text-centered is-flex is-align-items-center is-justify-content-center">
|
||||
<div class="goodbye-box">
|
||||
<div class="goodbye-title">Goodbye!</div>
|
||||
<p class="goodbye-subtitle">YTPTube has shut down.</p>
|
||||
<p class="goodbye-hint">You may now close this window.</p>
|
||||
<div class="goodbye-spinner" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.goodbye-box {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
background: radial-gradient(circle at top left, #6f42c1, #f66d9b);
|
||||
color: white;
|
||||
padding: 3rem 4rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0 50px rgba(255, 255, 255, 0.15);
|
||||
max-width: 480px;
|
||||
margin: 2rem auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.goodbye-title {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
animation: floatTitle 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.goodbye-subtitle {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.goodbye-hint {
|
||||
opacity: 0.75;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.goodbye-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 1rem auto 0;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
transform: translateY(40px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatTitle {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
ui/app/composables/useDialog.ts
Normal file
127
ui/app/composables/useDialog.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import {reactive, readonly} from 'vue'
|
||||
|
||||
export type DialogResult<T = string | null> = { status: boolean; value: T }
|
||||
|
||||
type BaseOptions = {
|
||||
/**
|
||||
* Title of the dialog
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* Message to display in the dialog
|
||||
*/
|
||||
message?: string
|
||||
/**
|
||||
* Text for the confirm button
|
||||
*/
|
||||
confirmText?: string
|
||||
}
|
||||
|
||||
export type PromptOptions = BaseOptions & {
|
||||
/**
|
||||
* Text for the input field
|
||||
*/
|
||||
initial?: string
|
||||
/**
|
||||
* Placeholder text for the input field
|
||||
*/
|
||||
placeholder?: string
|
||||
/**
|
||||
* Text for the cancel button
|
||||
*/
|
||||
cancelText?: string
|
||||
/**
|
||||
* Function to validate the input value
|
||||
* @returns true if valid, or an error message string if invalid
|
||||
*/
|
||||
validate?: (v: string) => true | string
|
||||
}
|
||||
|
||||
export type ConfirmOptions = BaseOptions & {
|
||||
/**
|
||||
* Text for the confirm button
|
||||
*/
|
||||
cancelText?: string
|
||||
}
|
||||
|
||||
export type AlertOptions = BaseOptions & {}
|
||||
|
||||
type QueueItem = {
|
||||
type: 'prompt' | 'confirm' | 'alert'
|
||||
opts: PromptOptions | ConfirmOptions | AlertOptions
|
||||
resolve: (r: DialogResult<any>) => void
|
||||
}
|
||||
|
||||
type DialogState = {
|
||||
current: QueueItem | null
|
||||
queue: QueueItem[]
|
||||
errorMsg: string | null
|
||||
input: string
|
||||
}
|
||||
|
||||
export const useDialog = () => {
|
||||
const raw = useState<DialogState>('dialog:state', () => reactive({
|
||||
current: null,
|
||||
queue: [],
|
||||
errorMsg: null,
|
||||
input: '',
|
||||
} as DialogState))
|
||||
|
||||
const state = raw.value
|
||||
|
||||
const _dequeue = () => {
|
||||
if (!state.current && state.queue.length) {
|
||||
state.current = state.queue.shift()!
|
||||
state.errorMsg = null
|
||||
state.input = state.current.type === 'prompt' ? (state.current.opts as PromptOptions).initial ?? '' : ''
|
||||
}
|
||||
}
|
||||
|
||||
const promptDialog = (opts: PromptOptions) => new Promise<DialogResult<string>>((resolve) => {
|
||||
state.queue.push({type: 'prompt', opts, resolve})
|
||||
_dequeue()
|
||||
})
|
||||
|
||||
const confirmDialog = (opts: ConfirmOptions) => new Promise<DialogResult<null>>((resolve) => {
|
||||
state.queue.push({type: 'confirm', opts, resolve})
|
||||
_dequeue()
|
||||
})
|
||||
|
||||
const alertDialog = (opts: AlertOptions) => new Promise<DialogResult<null>>((resolve) => {
|
||||
state.queue.push({type: 'alert', opts, resolve})
|
||||
_dequeue()
|
||||
})
|
||||
|
||||
const confirm = (value?: string) => {
|
||||
if (!state.current) return
|
||||
if (state.current.type === 'prompt') {
|
||||
const val = value ?? state.input
|
||||
const v = (state.current.opts as PromptOptions).validate?.(val)
|
||||
if (v && v !== true) {
|
||||
state.errorMsg = v
|
||||
return
|
||||
}
|
||||
state.current.resolve({status: true, value: val})
|
||||
} else {
|
||||
state.current.resolve({status: true, value: null})
|
||||
}
|
||||
state.current = null
|
||||
_dequeue()
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
if (!state.current) return
|
||||
state.current.resolve({status: false, value: null})
|
||||
state.current = null
|
||||
_dequeue()
|
||||
}
|
||||
|
||||
return {
|
||||
promptDialog,
|
||||
confirmDialog,
|
||||
alertDialog,
|
||||
confirm,
|
||||
cancel,
|
||||
state: readonly(state) as Readonly<DialogState>,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div id="main_container" class="container">
|
||||
<Shutdown v-if="app_shutdown" />
|
||||
<div id="main_container" class="container" v-else>
|
||||
<NewVersion v-if="newVersionIsAvailable" />
|
||||
<nav class="navbar is-mobile is-dark">
|
||||
|
||||
|
|
@ -75,7 +76,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item" v-if="false === config.app.is_native">
|
||||
<div class="navbar-item" v-if="true === config.app.is_native">
|
||||
<button class="button is-dark" @click="shutdownApp">
|
||||
<span class="icon"><i class="fas fa-power-off" /></span>
|
||||
<span v-if="isMobile">Shutdown</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="navbar-item">
|
||||
<button class="button is-dark" @click="reloadPage">
|
||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||
<span v-if="isMobile">Reload</span>
|
||||
|
|
@ -113,6 +122,9 @@
|
|||
connection is functional.
|
||||
</p>
|
||||
</Message>
|
||||
<ClientOnly>
|
||||
<Dialog />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
<div class="columns mt-3 is-mobile">
|
||||
|
|
@ -148,6 +160,9 @@ import { useStorage } from '@vueuse/core'
|
|||
import moment from 'moment'
|
||||
import * as Sentry from '@sentry/nuxt'
|
||||
import type { YTDLPOption } from '~/types/ytdlp'
|
||||
import { useDialog } from '~/composables/useDialog'
|
||||
import Dialog from '~/components/Dialog.vue'
|
||||
import Shutdown from '~/components/shutdown.vue'
|
||||
|
||||
const Year = new Date().getFullYear()
|
||||
const selectedTheme = useStorage('theme', 'auto')
|
||||
|
|
@ -160,6 +175,7 @@ const bg_enable = useStorage('random_bg', true)
|
|||
const bg_opacity = useStorage('random_bg_opacity', 0.95)
|
||||
const showMenu = ref(false)
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 })
|
||||
const app_shutdown = ref<boolean>(false)
|
||||
|
||||
const applyPreferredColorScheme = (scheme: string) => {
|
||||
if (!scheme || scheme === 'auto') {
|
||||
|
|
@ -388,6 +404,47 @@ const useVersionUpdate = () => {
|
|||
newVersionIsAvailable: readonly(newVersionIsAvailable),
|
||||
}
|
||||
}
|
||||
|
||||
const { newVersionIsAvailable } = useVersionUpdate()
|
||||
|
||||
const shutdownApp = async () => {
|
||||
const { alertDialog, confirmDialog: confirm_message } = useDialog()
|
||||
|
||||
if (false === config.app.is_native) {
|
||||
await alertDialog({
|
||||
title: 'Shutdown Unavailable',
|
||||
message: 'The shutdown feature is only available when running as native application.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { status } = await confirm_message({
|
||||
title: 'Shutdown Application',
|
||||
message: 'Are you sure you want to shutdown the application?',
|
||||
})
|
||||
|
||||
if (false === status) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/system/shutdown', { method: 'POST' })
|
||||
if (!resp.ok) {
|
||||
const body = await resp.json()
|
||||
await alertDialog({
|
||||
title: 'Shutdown Failed',
|
||||
message: `Failed to shutdown the application: ${body.error || resp.statusText || resp.status}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
app_shutdown.value = true
|
||||
await nextTick()
|
||||
} catch (e: any) {
|
||||
await alertDialog({
|
||||
title: 'Shutdown Failed',
|
||||
message: `Failed to shutdown the application: ${e.message || e}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
217
uv.lock
217
uv.lock
|
|
@ -182,15 +182,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bottle"
|
||||
version = "0.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.1.0"
|
||||
|
|
@ -390,18 +381,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clr-loader"
|
||||
version = "0.2.7.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/b3/8ae917e458394e2cebdbf17bed0a8204f8d4ffc79a093a7b1141c7731d3c/clr_loader-0.2.7.post0.tar.gz", hash = "sha256:b7a8b3f8fbb1bcbbb6382d887e21d1742d4f10b5ea209e4ad95568fe97e1c7c6", size = 56701 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/c0/06e64a54bced4e8b885c1e7ec03ee1869e52acf69e87da40f92391a214ad/clr_loader-0.2.7.post0-py3-none-any.whl", hash = "sha256:e0b9fcc107d48347a4311a28ffe3ae78c4968edb216ffb6564cb03f7ace0bb47", size = 50649 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
|
|
@ -845,12 +824,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxy-tools"
|
||||
version = "0.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010", size = 2978 }
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
|
|
@ -993,91 +966,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/0f/2a/faf915e22f136acf1588c26358f86b059fca23e4342e9fd69e41e06cba45/pyjson5-1.6.9-cp313-cp313-win_arm64.whl", hash = "sha256:fed7cf6605bbdb310395d81cc8abb4403ed88ae1f3c37256445e9c01ea073eab", size = 115836 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-core"
|
||||
version = "11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/e9/0b85c81e2b441267bca707b5d89f56c2f02578ef8f3eafddf0e0c0b8848c/pyobjc_core-11.1.tar.gz", hash = "sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe", size = 974602 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a7/55afc166d89e3fcd87966f48f8bca3305a3a2d7c62100715b9ffa7153a90/pyobjc_core-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ec36680b5c14e2f73d432b03ba7c1457dc6ca70fa59fd7daea1073f2b4157d33", size = 671075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/09/e83228e878e73bf756749939f906a872da54488f18d75658afa7f1abbab1/pyobjc_core-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:765b97dea6b87ec4612b3212258024d8496ea23517c95a1c5f0735f96b7fd529", size = 677985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/24/12e4e2dae5f85fd0c0b696404ed3374ea6ca398e7db886d4f1322eb30799/pyobjc_core-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:18986f83998fbd5d3f56d8a8428b2f3e0754fd15cef3ef786ca0d29619024f2c", size = 676431 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/79/031492497624de4c728f1857181b06ce8c56444db4d49418fa459cba217c/pyobjc_core-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8849e78cfe6595c4911fbba29683decfb0bf57a350aed8a43316976ba6f659d2", size = 719330 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/7d/6169f16a0c7ec15b9381f8bf33872baf912de2ef68d96c798ca4c6ee641f/pyobjc_core-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cb9ed17a8d84a312a6e8b665dd22393d48336ea1d8277e7ad20c19a38edf731", size = 667203 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0f/f5ab2b0e57430a3bec9a62b6153c0e79c05a30d77b564efdb9f9446eeac5/pyobjc_core-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:f2455683e807f8541f0d83fbba0f5d9a46128ab0d5cc83ea208f0bec759b7f96", size = 708807 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/7a866d24bc026f79239b74d05e2cf3088b03263da66d53d1b4cf5207f5ae/pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038", size = 5565335 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/43/6841046aa4e257b6276cd23e53cacedfb842ecaf3386bb360fa9cc319aa1/pyobjc_framework_cocoa-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b9a9b8ba07f5bf84866399e3de2aa311ed1c34d5d2788a995bdbe82cc36cfa0", size = 388177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/da/41c0f7edc92ead461cced7e67813e27fa17da3c5da428afdb4086c69d7ba/pyobjc_framework_cocoa-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806de56f06dfba8f301a244cce289d54877c36b4b19818e3b53150eb7c2424d0", size = 388983 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/0b/a01477cde2a040f97e226f3e15e5ffd1268fcb6d1d664885a95ba592eca9/pyobjc_framework_cocoa-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54e93e1d9b0fc41c032582a6f0834befe1d418d73893968f3f450281b11603da", size = 389049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/64cf2661f6ab7c124d0486ec6d1d01a9bb2838a0d2a46006457d8c5e6845/pyobjc_framework_cocoa-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd5245ee1997d93e78b72703be1289d75d88ff6490af94462b564892e9266350", size = 393110 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/87/01e35c5a3c5bbdc93d5925366421e10835fcd7b23347b6c267df1b16d0b3/pyobjc_framework_cocoa-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:aede53a1afc5433e1e7d66568cc52acceeb171b0a6005407a42e8e82580b4fc0", size = 392644 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/7c/54afe9ffee547c41e1161691e72067a37ed27466ac71c089bfdcd07ca70d/pyobjc_framework_cocoa-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:1b5de4e1757bb65689d6dc1f8d8717de9ec8587eb0c4831c134f13aba29f9b71", size = 396742 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-quartz"
|
||||
version = "11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/6308fec6c9ffeda9942fef72724f4094c6df4933560f512e63eac37ebd30/pyobjc_framework_quartz-11.1.tar.gz", hash = "sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75", size = 3953275 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/cb/38172fdb350b3f47e18d87c5760e50f4efbb4da6308182b5e1310ff0cde4/pyobjc_framework_quartz-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d501fe95ef15d8acf587cb7dc4ab4be3c5a84e2252017da8dbb7df1bbe7a72a", size = 215565 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/37/ee6e0bdd31b3b277fec00e5ee84d30eb1b5b8b0e025095e24ddc561697d0/pyobjc_framework_quartz-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ac806067541917d6119b98d90390a6944e7d9bd737f5c0a79884202327c9204", size = 216410 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/27/4f4fc0e6a0652318c2844608dd7c41e49ba6006ee5fb60c7ae417c338357/pyobjc_framework_quartz-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43a1138280571bbf44df27a7eef519184b5c4183a588598ebaaeb887b9e73e76", size = 216816 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/8a/1d15e42496bef31246f7401aad1ebf0f9e11566ce0de41c18431715aafbc/pyobjc_framework_quartz-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b23d81c30c564adf6336e00b357f355b35aad10075dd7e837cfd52a9912863e5", size = 221941 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a8/a3f84d06e567efc12c104799c7fd015f9bea272a75f799eda8b79e8163c6/pyobjc_framework_quartz-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:07cbda78b4a8fcf3a2d96e047a2ff01f44e3e1820f46f0f4b3b6d77ff6ece07c", size = 221312 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/ef/8c08d4f255bb3efe8806609d1f0b1ddd29684ab0f9ffb5e26d3ad7957b29/pyobjc_framework_quartz-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:39d02a3df4b5e3eee1e0da0fb150259476910d2a9aa638ab94153c24317a9561", size = 226353 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-security"
|
||||
version = "11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/6f/ba50ed2d9c1192c67590a7cfefa44fc5f85c776d1e25beb224dec32081f6/pyobjc_framework_security-11.1.tar.gz", hash = "sha256:dabcee6987c6bae575e2d1ef0fcbe437678c4f49f1c25a4b131a5e960f31a2da", size = 302291 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/ae/1679770d9a1cf5f2fe532a3567a51f0c5ee09054ae2c4003ae8f3e11eea4/pyobjc_framework_security-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d361231697486e97cfdafadf56709190696ab26a6a086dbba5f170e042e13daa", size = 41202 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/16/7fc52ab1364ada5885bf9b4c9ea9da3ad892b847c9b86aa59e086b16fc11/pyobjc_framework_security-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eb4ba6d8b221b9ad5d010e026247e8aa26ee43dcaf327e848340ed227d22d7e", size = 41222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/d8/cb20b4c4d15b2bdc7e39481159e50a933ddb87e4702d35060c254b316055/pyobjc_framework_security-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:158da3b2474e2567fd269531c4ee9f35b8ba4f1eccbd1fb4a37c85a18bf1243c", size = 41221 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3c/d13d6870f5d66f5379565887b332f86f16d666dc50a1944d7e3a1462e76c/pyobjc_framework_security-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:141cc3ee08627ae0698264efc3dbbaf28d2255e0fe690e336eb8f0f387c4af01", size = 42099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3d/2f61d4566e80f203d0e05ddd788037dc06a94d200edac25d2747fd79b5aa/pyobjc_framework_security-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:858a18303711eb69d18d1a64cf8bb2202f64a3bd1c82203c511990dbd8326514", size = 41288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/44/99ef33a5319ed2cb6c0a51ed36214adf21ccb37cce970b1acc8bfe57ce23/pyobjc_framework_security-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:4db1ebf6395cd370139cb35ff172505fc449c7fdf5d3a28f2ada8a30ef132cd0", size = 42849 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-webkit"
|
||||
version = "11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/04/fb3d0b68994f7e657ef00c1ac5fc1c04ae2fc7ea581d647f5ae1f6739b14/pyobjc_framework_webkit-11.1.tar.gz", hash = "sha256:27e701c7aaf4f24fc7e601a128e2ef14f2773f4ab071b9db7438dc5afb5053ae", size = 717102 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/b6/d62c01a83c22619edf2379a6941c9f6b7aee01c565b9c1170696f85cba95/pyobjc_framework_webkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:10ec89d727af8f216ba5911ff5553f84a5b660f5ddf75b07788e3a439c281165", size = 51406 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/7e/fa2c18c0c0f9321e5036e54b9da7a196956b531e50fe1a76e7dfdbe8fac2/pyobjc_framework_webkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1a6e6f64ca53c4953f17e808ecac11da288d9a6ade738156ba161732a5e0c96a", size = 51464 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/8d/66561d95b00b8e57a9d5725ae34a8d9ca7ebeb776f13add989421ff90279/pyobjc_framework_webkit-11.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d01008756c3912b02b7c02f62432467fbee90a93e3b8e31fa351b4ca97c9c98", size = 51495 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c3/e790b518f84ea8dfbe32a9dcb4d8611b532de08057d19f853c1890110938/pyobjc_framework_webkit-11.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:864f9867a2caaeaeb83e5c0fa3dcf78169622233cf93a9a5eeb7012ced3b8076", size = 51985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/4f/194e3e7c01861a5e46dfe9e1fa28ad01fd07190cb514e41a7dcf1f0b7031/pyobjc_framework_webkit-11.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:13b774d4244734cb77bf3c3648149c163f62acaa105243d7c48bb3fd856b5628", size = 52248 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/09/28884e7c10d3a76a76c2c8f55369dd96a90f0283800c68f5c764e1fb8e2e/pyobjc_framework_webkit-11.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:c1c00d549ab1d50e3d7e8f5f71352b999d2c32dc2365c299f317525eb9bff916", size = 52725 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyreadline3"
|
||||
version = "3.5.4"
|
||||
|
|
@ -1087,45 +975,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6"
|
||||
version = "6.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyside6-addons" },
|
||||
{ name = "pyside6-essentials" },
|
||||
{ name = "shiboken6" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ff/95c941f53b0faebc27dbe361d8e971b77f504b9cf36f8f5d750fd82cd6fc/PySide6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:c82dbb7d32bbdd465e01059174f71bddc97de152ab71bded3f1907c40f9a5f16", size = 564571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/ef/0aa5e910fa4e9770db6b45c23e360a52313922e0ca71fc060a57db613de1/PySide6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:1525d63dc6dc425b8c2dc5bc01a8cb1d67530401449f3a3490c09a14c095b9f9", size = 401793 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-addons"
|
||||
version = "6.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyside6-essentials" },
|
||||
{ name = "shiboken6" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/be/a849402f7e73d137b5ae8b4370a49b0cf0e0c02f028b845782cb743e4995/PySide6_Addons-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:cd93a3a5e3886cd958f3a5acc7c061c24f10a394ce9f4ce657ac394544ca7ec2", size = 143150906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/f1/1bb6b5859aff4e2b3f5ef789b9cee200811a9f469f04d9aa7425e816622b/PySide6_Addons-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:4f589631bdceb518080ae9c9fa288e64f092cd5bebe25adc8ad89e8eadd4db29", size = 26938762 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-essentials"
|
||||
version = "6.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "shiboken6" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/a9/a8e0209ba9116f2c2db990cfb79f2edbd5a3a428013be2df1f1cddd660a9/PySide6_Essentials-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:ad1ac94011492dba33051bc33db1c76a7d6f815a81c01422cb6220273b369145", size = 72435676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/e4/23268c57e775a1a4d2843d288a9583a47f2e4b3977a9ae93cb9ded1a4ea5/PySide6_Essentials-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:35c2c2bb4a88db74d11e638cf917524ff35785883f10b439ead07960a5733aa4", size = 49483707 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pysubs2"
|
||||
version = "1.8.0"
|
||||
|
|
@ -1199,18 +1048,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/3c/32/b4fb8585d1be0f68bde7e110dffbcf354915f77ad8c778563f0ad9655c02/python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf", size = 77800 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pythonnet"
|
||||
version = "3.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "clr-loader" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
|
|
@ -1220,27 +1057,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywebview"
|
||||
version = "6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bottle" },
|
||||
{ name = "proxy-tools" },
|
||||
{ name = "pyobjc-core", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pythonnet", marker = "sys_platform == 'win32'" },
|
||||
{ name = "qtpy", marker = "sys_platform == 'openbsd6'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/2a/0279e9da655481cef5dd0cdb1f9b8bfbe8fdf81d0062731f8e1e6668898d/pywebview-6.0.tar.gz", hash = "sha256:05c339ace3ee1c11391aa9ae8c4645c67239e769c91228f63196d8d120a9feb8", size = 493594 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/74/611f560a08c5d62fe5a528f784f71aefa4753a52b448489be92f310a3748/pywebview-6.0-py3-none-any.whl", hash = "sha256:6164b2a3debc416eeba15f3a48b1febd0b9483a6674c933c7c9b59fb4b11f474", size = 508599 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
|
|
@ -1285,18 +1101,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qtpy"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2025.7.34"
|
||||
|
|
@ -1398,15 +1202,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shiboken6"
|
||||
version = "6.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/30/56/00af281275aab4c79e22e0ea65feede0a5c6da3b84e86b21a4a0071e0744/shiboken6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:209ccf02c135bd70321143dcbc5023ae0c056aa4850a845955dd2f9b2ff280a9", size = 1153587 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/ce/6ccd382fbe1a96926c5514afa6f2c42da3a9a8482e61f8dfc6068a9ca64f/shiboken6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:2a39997ce275ced7853defc89d3a1f19a11c90991ac6eef3435a69bb0b7ff1de", size = 1831623 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple-websocket"
|
||||
version = "1.1.0"
|
||||
|
|
@ -1616,11 +1411,8 @@ dependencies = [
|
|||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
webview = [
|
||||
installer = [
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "pyside6", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pywebview" },
|
||||
{ name = "qtpy", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
|
|
@ -1644,22 +1436,19 @@ requires-dist = [
|
|||
{ name = "mutagen" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "pyinstaller", marker = "extra == 'webview'" },
|
||||
{ name = "pyinstaller", marker = "extra == 'installer'" },
|
||||
{ name = "pyjson5" },
|
||||
{ name = "pyside6", marker = "sys_platform == 'win32' and extra == 'webview'" },
|
||||
{ name = "pysubs2" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||
{ name = "python-magic", marker = "sys_platform != 'win32'", specifier = ">=0.4.27" },
|
||||
{ name = "python-magic-bin", marker = "sys_platform == 'win32'" },
|
||||
{ name = "python-socketio", specifier = ">=5.11.1" },
|
||||
{ name = "pywebview", marker = "extra == 'webview'" },
|
||||
{ name = "qtpy", marker = "sys_platform == 'win32' and extra == 'webview'" },
|
||||
{ name = "regex" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
{ name = "yt-dlp" },
|
||||
{ name = "zipstream-ng", specifier = ">=1.8.0" },
|
||||
]
|
||||
provides-extras = ["webview"]
|
||||
provides-extras = ["installer"]
|
||||
|
||||
[[package]]
|
||||
name = "zipstream-ng"
|
||||
|
|
|
|||
Loading…
Reference in a new issue