From f339c35c5a06d58d625a2032089d7dce2c42dca5 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Sun, 5 Nov 2023 22:37:16 +0100 Subject: [PATCH 01/18] Experiments and prepatations for gui --- README.md | 1 + docs/source/json.rst | 2 +- pyproject.toml | 4 +- syng/client.py | 126 +++++++++------- syng/gui.py | 243 +++++++++++++++++++++++++++++++ syng/{json.py => jsonencoder.py} | 0 syng/server.py | 18 ++- syng/sources/filebased.py | 76 ++++++++++ syng/sources/files.py | 42 +++--- syng/sources/s3.py | 107 +++++--------- syng/sources/source.py | 15 +- syng/sources/youtube.py | 13 +- 12 files changed, 484 insertions(+), 163 deletions(-) create mode 100644 syng/gui.py rename syng/{json.py => jsonencoder.py} (100%) create mode 100644 syng/sources/filebased.py diff --git a/README.md b/README.md index e69de29..58ea28e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1 @@ +# Syng diff --git a/docs/source/json.rst b/docs/source/json.rst index f8e2903..e1df64f 100644 --- a/docs/source/json.rst +++ b/docs/source/json.rst @@ -1,5 +1,5 @@ JSON ==== -.. automodule:: syng.json +.. automodule:: syng.jsonencoder :members: diff --git a/pyproject.toml b/pyproject.toml index b95faff..f2b9808 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,9 @@ mutagen = "^1.46.0" aiocmd = "^0.1.5" pillow = "^9.3.0" yt-dlp = "*" -pyqrcodeng = "^1.3.6" +customtkinter = "^5.2.1" +qrcode = "^7.4.2" +pymediainfo = "^6.1.0" [build-system] requires = ["poetry-core"] diff --git a/syng/client.py b/syng/client.py index 30e68d1..de8143b 100644 --- a/syng/client.py +++ b/syng/client.py @@ -44,17 +44,18 @@ from traceback import print_exc from typing import Any from typing import Optional -import pyqrcodeng as pyqrcode +import qrcode + import socketio from PIL import Image -from . import json +from . import jsonencoder from .entry import Entry from .sources import configure_sources from .sources import Source -sio: socketio.AsyncClient = socketio.AsyncClient(json=json) +sio: socketio.AsyncClient = socketio.AsyncClient(json=jsonencoder) logger: logging.Logger = logging.getLogger(__name__) sources: dict[str, Source] = {} @@ -87,15 +88,13 @@ class State: :type waiting_room: list[Entry] :param recent: A copy of all played songs this session. :type recent: list[Entry] - :param room: The room on the server this playback client is connected to. - :type room: str - :param secret: The passcode of the room. If a playback client reconnects to - a room, this must be identical. Also, if a webclient wants to have - admin privileges, this must be included. - :type secret: str - :param key: An optional key, if registration on the server is limited. - :type key: Optional[str] :param config: Various configuration options for the client: + * `server` (`str`): The url of the server to connect to. + * `room` (`str`): The room on the server this playback client is connected to. + * `secret` (`str`): The passcode of the room. If a playback client reconnects to + a room, this must be identical. Also, if a webclient wants to have + admin privileges, this must be included. + * `key` (`Optional[str]`) An optional key, if registration on the server is limited. * `preview_duration` (`Optional[int]`): The duration in seconds the playback client shows a preview for the next song. This is accounted for in the calculation of the ETA for songs later in the queue. @@ -116,10 +115,6 @@ class State: queue: list[Entry] = field(default_factory=list) waiting_room: list[Entry] = field(default_factory=list) recent: list[Entry] = field(default_factory=list) - room: str = "" - server: str = "" - secret: str = "" - key: Optional[str] = None config: dict[str, Any] = field(default_factory=default_config) @@ -200,12 +195,12 @@ async def handle_connect() -> None: "queue": state.queue, "waiting_room": state.waiting_room, "recent": state.recent, - "room": state.room, - "secret": state.secret, + # "room": state.config["room"], + # "secret": state.config["secret"], "config": state.config, } - if state.key: - data["registration-key"] = state.key + if state.config["key"]: + data["registration-key"] = state.config["key"] # TODO: unify await sio.emit("register-client", data) @@ -304,7 +299,7 @@ async def handle_play(data: dict[str, Any]) -> None: @sio.on("client-registered") async def handle_client_registered(data: dict[str, Any]) -> None: """ - Handle the "client-registered" massage. + Handle the "client-registered" message. If the registration was successfull (`data["success"]` == `True`), store the room code in the global :py:class:`State` and print out a link to join @@ -325,9 +320,12 @@ async def handle_client_registered(data: dict[str, Any]) -> None: """ if data["success"]: logging.info("Registered") - print(f"Join here: {state.server}/{data['room']}") - print(pyqrcode.create(f"{state.server}/{data['room']}").terminal(quiet_zone=1)) - state.room = data["room"] + print(f"Join here: {state.config['server']}/{data['room']}") + qr = qrcode.QRCode(box_size=20, border=2) + qr.add_data(f"{state.config['server']}/{data['room']}") + qr.make() + qr.print_ascii() + state.config["room"] = data["room"] await sio.emit("sources", {"sources": list(sources.keys())}) if state.current_source is None: # A possible race condition can occur here await sio.emit("get-first") @@ -373,30 +371,14 @@ async def handle_request_config(data: dict[str, Any]) -> None: await sio.emit("config", {"source": data["source"], "config": config}) -async def aiomain() -> None: +async def start_client(config: dict[str, Any]) -> None: """ - Async main function. - - Parses the arguments, reads a config file and sets default values. Then - connects to a specified server. - - If no secret is given, a random secret will be generated and presented to - the user. + Initialize the client and connect to the server. + :param config: Config options for the client + :type config: dict[str, Any] :rtype: None """ - parser: ArgumentParser = ArgumentParser() - - parser.add_argument("--room", "-r") - parser.add_argument("--secret", "-s") - parser.add_argument("--config-file", "-C", default="syng-client.json") - parser.add_argument("--key", "-k", default=None) - parser.add_argument("server") - - args = parser.parse_args() - - with open(args.config_file, encoding="utf8") as file: - config = load(file) sources.update(configure_sources(config["sources"])) if "config" in config: @@ -407,28 +389,60 @@ async def aiomain() -> None: ) state.config |= config["config"] | {"last_song": last_song} - state.key = args.key if args.key else None - - if args.room: - state.room = args.room - - if args.secret: - state.secret = args.secret - else: - state.secret = "".join( + if not ("secret" in state.config and state.config["secret"]): + state.config["secret"] = "".join( secrets.choice(string.ascii_letters + string.digits) for _ in range(8) ) - print(f"Generated secret: {state.secret}") + print(f"Generated secret: {state.config['secret']}") - state.server = args.server + if not ("key" in state.config and state.config["key"]): + state.config["key"] = "" - await sio.connect(args.server) + await sio.connect(state.config["server"]) await sio.wait() +async def aiomain() -> None: + """ + Async main function. + + Parses the arguments, reads a config file and sets default values. Then + connects to a specified server. + + If no secret is given, a random secret will be generated and presented to + the user. + + """ + pass + + def main() -> None: """Entry point for the syng-client script.""" - asyncio.run(aiomain()) + parser: ArgumentParser = ArgumentParser() + + parser.add_argument("--room", "-r") + parser.add_argument("--secret", "-s") + parser.add_argument("--config-file", "-C", default="syng-client.json") + parser.add_argument("--key", "-k", default=None) + parser.add_argument("--server", "-S") + + args = parser.parse_args() + + with open(args.config_file, encoding="utf8") as file: + config = load(file) + + if "config" not in config: + config["config"] = {} + + config["config"] |= {"key": args.key} + if args.room: + config["config"] |= {"room": args.room} + if args.secret: + config["config"] |= {"secret": args.secret} + if args.server: + config["config"] |= {"server": args.server} + + asyncio.run(start_client(config)) if __name__ == "__main__": diff --git a/syng/gui.py b/syng/gui.py new file mode 100644 index 0000000..fceb4b6 --- /dev/null +++ b/syng/gui.py @@ -0,0 +1,243 @@ +import builtins +from functools import partial +from json import load +import customtkinter +import qrcode +import secrets +import string +from tkinter import filedialog + +from syng.client import default_config + +from .sources import available_sources + + +class SyngGui(customtkinter.CTk): + def loadConfig(self): + filedialog.askopenfilename() + + def __init__(self): + super().__init__(className="Syng") + + self.wm_title("Syng") + tabview = customtkinter.CTkTabview(self) + tabview.pack(side="top") + + tabview.add("General") + for source in available_sources: + tabview.add(source) + tabview.set("General") + + fileframe = customtkinter.CTkFrame(self) + fileframe.pack(side="bottom") + + loadbutton = customtkinter.CTkButton( + fileframe, + text="load", + command=self.loadConfig, + ) + loadbutton.pack(side="left") + + frm = customtkinter.CTkFrame(tabview.tab("General")) + frm.grid(ipadx=10) + + self.qrlabel = customtkinter.CTkLabel(frm, text="") + self.qrlabel.grid(column=0, row=0) + + optionframe = customtkinter.CTkFrame(frm) + optionframe.grid(column=1, row=0) + + customtkinter.CTkLabel(optionframe, text="Server", justify="left").grid( + column=0, row=0, padx=5, pady=5 + ) + self.serverTextbox = customtkinter.CTkTextbox( + optionframe, wrap="none", height=1 + ) + self.serverTextbox.grid(column=1, row=0) + self.serverTextbox.bind("", self.updateQr) + + customtkinter.CTkLabel(optionframe, text="Room", justify="left").grid( + column=0, row=1 + ) + self.roomTextbox = customtkinter.CTkTextbox(optionframe, wrap="none", height=1) + self.roomTextbox.grid(column=1, row=1) + self.roomTextbox.bind("", self.updateQr) + + customtkinter.CTkLabel(optionframe, text="Secret", justify="left").grid( + column=0, row=3 + ) + self.secretTextbox = customtkinter.CTkTextbox( + optionframe, wrap="none", height=1 + ) + secret = "".join( + secrets.choice(string.ascii_letters + string.digits) for _ in range(8) + ) + self.secretTextbox.grid(column=1, row=3) + + customtkinter.CTkLabel( + optionframe, text="Waiting room policy", justify="left" + ).grid(column=0, row=4) + self.waitingRoomPolicy = customtkinter.CTkOptionMenu( + optionframe, values=["forced", "optional", "none"] + ) + self.waitingRoomPolicy.set("none") + self.waitingRoomPolicy.grid(column=1, row=4) + + customtkinter.CTkLabel( + optionframe, text="Time of last Song", justify="left" + ).grid(column=0, row=5) + self.last_song = customtkinter.CTkTextbox(optionframe, wrap="none", height=1) + self.last_song.grid(column=1, row=5) + + customtkinter.CTkLabel( + optionframe, text="Preview Duration", justify="left" + ).grid(column=0, row=6) + self.preview_duration = customtkinter.CTkTextbox( + optionframe, wrap="none", height=1 + ) + self.preview_duration.grid(column=1, row=6) + + customtkinter.CTkButton(optionframe, text="Start", command=self.start).grid( + column=0, row=7, columnspan=2, pady=10 + ) + + with open("syng-client.json") as cfile: + loaded_config = load(cfile) + config = {"sources": {}, "config": {}} + + self.source_config_elements = {} + for source_name, source in available_sources.items(): + self.source_config_elements[source_name] = {} + config["sources"][source_name] = {} + sourcefrm = customtkinter.CTkFrame(tabview.tab(source_name)) + sourcefrm.grid(ipadx=10) + for row, (name, (typ, desc, default)) in enumerate( + source.config_schema.items() + ): + if name in loaded_config["sources"][source_name]: + config["sources"][source_name][name] = loaded_config["sources"][ + source_name + ][name] + else: + config["sources"][source_name][name] = default + + label = customtkinter.CTkLabel( + sourcefrm, text=f"{desc} ({name})", justify="right" + ) + label.grid(column=0, row=row) + match typ: + case builtins.bool: + self.source_config_elements[source_name][ + name + ] = customtkinter.CTkSwitch(sourcefrm, text="") + self.source_config_elements[source_name][name].grid( + column=1, row=row + ) + if config["sources"][source_name][name]: + self.source_config_elements[source_name][name].select() + else: + self.source_config_elements[source_name][name].deselect() + + case builtins.list: + self.source_config_elements[source_name][ + name + ] = customtkinter.CTkTextbox(sourcefrm, wrap="none", height=1) + self.source_config_elements[source_name][name].grid( + column=1, row=row + ) + self.source_config_elements[source_name][name].insert( + "0.0", ",".join(config["sources"][source_name][name]) + ) + + case _: + self.source_config_elements[source_name][ + name + ] = customtkinter.CTkTextbox(sourcefrm, wrap="none", height=1) + self.source_config_elements[source_name][name].grid( + column=1, row=row + ) + self.source_config_elements[source_name][name].insert( + "0.0", config["sources"][source_name][name] + ) + if source_name in loaded_config["sources"]: + config["sources"][source_name] |= loaded_config["sources"][source_name] + + if "config" in loaded_config: + config["config"] = default_config() | loaded_config["config"] + + self.serverTextbox.insert("0.0", config["config"]["server"]) + self.roomTextbox.insert("0.0", config["config"]["room"]) + self.secretTextbox.insert("0.0", config["config"]["secret"]) + self.waitingRoomPolicy.set(str(config["config"]["waiting_room_policy"]).lower()) + if config["config"]["last_song"]: + self.last_song.insert("0.0", config["config"]["last_song"]) + self.preview_duration.insert("0.0", config["config"]["preview_duration"]) + + self.updateQr() + + def start(self): + config = {} + config["server"] = self.serverTextbox.get("0.0", "end").strip() + config["room"] = self.roomTextbox.get("0.0", "end").strip() + config["secret"] = self.secretTextbox.get("0.0", "end").strip() + config["waiting_room_policy"] = self.waitingRoomPolicy.get().strip() + config["last_song"] = self.last_song.get("0.0", "end").strip() + try: + config["preview_duration"] = int( + self.preview_duration.get("0.0", "end").strip() + ) + except ValueError: + config["preview_duration"] = 0 + + sources = {} + for source_name, config_elements in self.source_config_elements.items(): + sources[source_name] = {} + for option, config_element in config_elements.items(): + if isinstance(config_element, customtkinter.CTkSwitch): + sources[source_name][option] = ( + True if config_element.get() == 1 else False + ) + elif isinstance(config_element, customtkinter.CTkTextbox): + match available_sources[source_name].config_schema[option][0]: + case builtins.list: + sources[source_name][option] = [ + value.strip() + for value in config_element.get("0.0", "end") + .strip() + .split(",") + ] + + case builtins.str: + sources[source_name][option] = config_element.get( + "0.0", "end" + ).strip() + else: + raise RuntimeError("IDK") + + syng_config = {"sources": sources, "config": config} + + print(syng_config) + + def changeQr(self, data: str): + qr = qrcode.QRCode(box_size=20, border=2) + qr.add_data(data) + qr.make() + qr.print_ascii() + image = qr.make_image().convert("RGB") + tkQrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) + self.qrlabel.configure(image=tkQrcode) + + def updateQr(self, _evt=None): + server = self.serverTextbox.get("0.0", "end").strip() + server += "" if server.endswith("/") else "/" + room = self.roomTextbox.get("0.0", "end").strip() + print(server + room) + self.changeQr(server + room) + + +def main(): + SyngGui().mainloop() + + +if __name__ == "__main__": + main() diff --git a/syng/json.py b/syng/jsonencoder.py similarity index 100% rename from syng/json.py rename to syng/jsonencoder.py diff --git a/syng/server.py b/syng/server.py index 32a253f..157bf1c 100644 --- a/syng/server.py +++ b/syng/server.py @@ -32,14 +32,14 @@ from typing import Optional import socketio from aiohttp import web -from . import json +from . import jsonencoder from .entry import Entry from .queue import Queue from .sources import available_sources from .sources import Source sio = socketio.AsyncServer( - cors_allowed_origins="*", logger=True, engineio_logger=False, json=json + cors_allowed_origins="*", logger=True, engineio_logger=False, json=jsonencoder ) app = web.Application() sio.attach(app) @@ -127,7 +127,6 @@ class State: :type client: Client """ - secret: str queue: Queue waiting_room: list[Entry] recent: list[Entry] @@ -329,7 +328,7 @@ async def handle_update_config(sid: str, data: dict[str, Any]) -> None: if is_admin: try: - config = json.loads(data["config"]) + config = jsonencoder.loads(data["config"]) await sio.emit( "update_config", DEFAULT_CONFIG | config, @@ -707,13 +706,17 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: ) return - room: str = data["room"] if "room" in data and data["room"] else gen_id() + room: str = ( + data["config"]["room"] + if "room" in data["config"] and data["config"]["room"] + else gen_id() + ) async with sio.session(sid) as session: session["room"] = room if room in clients: old_state: State = clients[room] - if data["secret"] == old_state.secret: + if data["config"]["secret"] == old_state.client.config["secret"]: logger.info("Got new client connection for %s", room) old_state.sid = sid old_state.client = Client( @@ -738,7 +741,6 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: initial_recent = [Entry(**entry) for entry in data["recent"]] clients[room] = State( - secret=data["secret"], queue=Queue(initial_entries), waiting_room=initial_waiting_room, recent=initial_recent, @@ -902,7 +904,7 @@ async def handle_register_admin(sid: str, data: dict[str, Any]) -> bool: room = session["room"] state = clients[room] - is_admin: bool = data["secret"] == state.secret + is_admin: bool = data["secret"] == state.client.config["secret"] async with sio.session(sid) as session: session["admin"] = is_admin return is_admin diff --git a/syng/sources/filebased.py b/syng/sources/filebased.py new file mode 100644 index 0000000..537b66f --- /dev/null +++ b/syng/sources/filebased.py @@ -0,0 +1,76 @@ +"""Module for an abstract filebased Source.""" +import asyncio +import os +from typing import Any, Optional + +from pymediainfo import MediaInfo + +from .source import Source + + +class FileBasedSource(Source): + """A source for indexing and playing songs from a local folder. + + Config options are: + -``dir``, dirctory to index and server from. + """ + + config_schema = Source.config_schema | { + "extensions": ( + list, + "List of filename extensions (mp3+cdg, mp4, ...)", + "mp3+cdg", + ) + } + + def __init__(self, config: dict[str, Any]): + """Initialize the file module.""" + super().__init__(config) + + self.extensions: list[str] = ( + config["extensions"] if "extensions" in config else ["mp3+cdg"] + ) + self.extra_mpv_arguments = ["--scale=oversample"] + + def has_correct_extension(self, path: str) -> bool: + """Check if a `path` has a correct extension. + + For A+B type extensions (like mp3+cdg) only the latter halve is checked + + :return: True iff path has correct extension. + :rtype: bool + """ + return os.path.splitext(path)[1][1:] in [ + ext.split("+")[-1] for ext in self.extensions + ] + + def get_video_audio_split(self, path: str) -> tuple[str, Optional[str]]: + extension_of_path = os.path.splitext(path)[1][1:] + splitted_extensions = [ext.split("+") for ext in self.extensions if "+" in ext] + splitted_extensions_dict = { + video: audio for [audio, video] in splitted_extensions + } + + if extension_of_path in splitted_extensions_dict: + audio_path = ( + os.path.splitext(path)[0] + + "." + + splitted_extensions_dict[extension_of_path] + ) + return (path, audio_path) + return (path, None) + + async def get_duration(self, path: str) -> int: + def _get_duration(file: str) -> int: + print(file) + info: str | MediaInfo = MediaInfo.parse(file) + if isinstance(info, str): + return 180 + return info.audio_tracks[0].to_data()["duration"] // 1000 + + video_path, audio_path = self.get_video_audio_split(path) + + check_path = audio_path if audio_path is not None else video_path + duration = await asyncio.to_thread(_get_duration, check_path) + + return duration diff --git a/syng/sources/files.py b/syng/sources/files.py index 7d92454..f5775e2 100644 --- a/syng/sources/files.py +++ b/syng/sources/files.py @@ -1,39 +1,41 @@ """Module for the files Source.""" import asyncio import os -from typing import Any +from typing import Any, Optional from typing import Tuple -import mutagen - from ..entry import Entry from .source import available_sources -from .source import Source +from .filebased import FileBasedSource -class FilesSource(Source): +class FilesSource(FileBasedSource): """A source for indexing and playing songs from a local folder. Config options are: - -``dir``, dirctory to index and server from. + -``dir``, dirctory to index and serve from. """ + source_name = "files" + config_schema = FileBasedSource.config_schema | { + "dir": (str, "Directory to index", ".") + } + def __init__(self, config: dict[str, Any]): """Initialize the file module.""" super().__init__(config) - self.source_name = "files" self.dir = config["dir"] if "dir" in config else "." self.extra_mpv_arguments = ["--scale=oversample"] async def get_file_list(self) -> list[str]: - """Collect all ``cdg`` files in ``dir``.""" + """Collect all files in ``dir``, that have the correct filename extension""" def _get_file_list() -> list[str]: file_list = [] for path, _, files in os.walk(self.dir): for file in files: - if file.endswith(".cdg"): + if self.has_correct_extension(file): file_list.append(os.path.join(path, file)[len(self.dir) :]) return file_list @@ -41,35 +43,27 @@ class FilesSource(Source): async def get_missing_metadata(self, entry: Entry) -> dict[str, Any]: """ - Return the duration for the mp3 file. + Return the duration for the entry file. - :param entry: The entry with the associated mp3 file + :param entry: An entry :type entry: Entry :return: A dictionary containing the duration in seconds in the ``duration`` key. :rtype: dict[str, Any] """ - def mutagen_wrapped(file: str) -> int: - meta_infos = mutagen.File(file).info - return int(meta_infos.length) + duration = await self.get_duration(os.path.join(self.dir, entry.ident)) - audio_file_name: str = os.path.join(self.dir, entry.ident[:-3] + "mp3") + return {"duration": duration} - duration = await asyncio.to_thread(mutagen_wrapped, audio_file_name) - - return {"duration": int(duration)} - - async def do_buffer(self, entry: Entry) -> Tuple[str, str]: + async def do_buffer(self, entry: Entry) -> Tuple[str, Optional[str]]: """ No buffering needs to be done, since the files are already on disk. - We just return the cdg file name and the inferred mp3 file name + We just return the file names. """ - video_file_name: str = os.path.join(self.dir, entry.ident) - audio_file_name: str = os.path.join(self.dir, entry.ident[:-3] + "mp3") - return video_file_name, audio_file_name + return self.get_video_audio_split(os.path.join(self.dir, entry.ident)) available_sources["files"] = FilesSource diff --git a/syng/sources/s3.py b/syng/sources/s3.py index af3bc90..684d962 100644 --- a/syng/sources/s3.py +++ b/syng/sources/s3.py @@ -12,15 +12,15 @@ from typing import cast from typing import Optional from typing import Tuple -import mutagen from minio import Minio +from .filebased import FileBasedSource + from ..entry import Entry from .source import available_sources -from .source import Source -class S3Source(Source): +class S3Source(FileBasedSource): """A source for playing songs from a s3 compatible storage. Config options are: @@ -31,14 +31,21 @@ class S3Source(Source): - ``index_file``: If the file does not exist, saves the paths of files from the s3 instance to this file. If it exists, loads the list of files from this file. - - ``extensions``: List of filename extensions. Index only files with these one - of these extensions (Default: ["cdg"]) """ + source_name = "s3" + config_schema = FileBasedSource.config_schema | { + "endpoint": (str, "Endpoint of the s3", ""), + "access_key": (str, "Access Key of the s3", ""), + "secret_key": (str, "Secret Key of the s3", ""), + "secure": (bool, "Use SSL", True), + "bucket": (str, "Bucket of the s3", ""), + "tmp_dir": (str, "Folder for temporary download", "/tmp/syng"), + } + def __init__(self, config: dict[str, Any]): """Create the source.""" super().__init__(config) - self.source_name = "s3" if "endpoint" in config and "access_key" in config and "secret_key" in config: self.minio: Minio = Minio( @@ -52,12 +59,6 @@ class S3Source(Source): config["tmp_dir"] if "tmp_dir" in config else "/tmp/syng" ) - self.extensions = ( - [f".{ext}" for ext in config["extensions"]] - if "extensions" in config - else [".cdg"] - ) - self.index_file: Optional[str] = ( config["index_file"] if "index_file" in config else None ) @@ -83,7 +84,7 @@ class S3Source(Source): file_list = [ obj.object_name for obj in self.minio.list_objects(self.bucket, recursive=True) - if os.path.splitext(obj.object_name)[1] in self.extensions + if self.has_correct_extension(obj.object_name) ] if self.index_file is not None and not os.path.isfile(self.index_file): with open(self.index_file, "w", encoding="utf8") as index_file_handle: @@ -103,20 +104,13 @@ class S3Source(Source): :rtype: dict[str, Any] """ - def mutagen_wrapped(file: str) -> int: - meta_infos = mutagen.File(file).info - return int(meta_infos.length) - await self.ensure_playable(entry) - audio_file_name: Optional[str] = self.downloaded_files[entry.ident].audio + file_name: Optional[str] = self.downloaded_files[entry.ident].video - if audio_file_name is None: - duration: int = 180 - else: - duration = await asyncio.to_thread(mutagen_wrapped, audio_file_name) + duration = await self.get_duration(file_name) - return {"duration": int(duration)} + return {"duration": duration} async def do_buffer(self, entry: Entry) -> Tuple[str, Optional[str]]: """ @@ -132,56 +126,31 @@ class S3Source(Source): :rtype: Tuple[str, Optional[str]] """ - if os.path.splitext(entry.ident)[1] == ".cdg": - cdg_filename: str = os.path.basename(entry.ident) - path_to_files: str = os.path.dirname(entry.ident) - - cdg_path: str = os.path.join(path_to_files, cdg_filename) - target_file_cdg: str = os.path.join(self.tmp_dir, cdg_path) - - ident_mp3: str = entry.ident[:-3] + "mp3" - target_file_mp3: str = target_file_cdg[:-3] + "mp3" - os.makedirs(os.path.dirname(target_file_cdg), exist_ok=True) - - cdg_task: asyncio.Task[Any] = asyncio.create_task( - asyncio.to_thread( - self.minio.fget_object, - self.bucket, - entry.ident, - target_file_cdg, - ) - ) - audio_task: asyncio.Task[Any] = asyncio.create_task( - asyncio.to_thread( - self.minio.fget_object, - self.bucket, - ident_mp3, - target_file_mp3, - ) - ) - - await cdg_task - await audio_task - return target_file_cdg, target_file_mp3 - video_filename: str = os.path.basename(entry.ident) - path_to_file: str = os.path.dirname(entry.ident) - - video_path: str = os.path.join(path_to_file, video_filename) - target_file_video: str = os.path.join(self.tmp_dir, video_path) - - os.makedirs(os.path.dirname(target_file_video), exist_ok=True) - - video_task: asyncio.Task[Any] = asyncio.create_task( + video_path, audio_path = self.get_video_audio_split(entry.ident) + video_dl_path: str = os.path.join(self.tmp_dir, video_path) + os.makedirs(os.path.dirname(video_dl_path), exist_ok=True) + video_dl_task: asyncio.Task[Any] = asyncio.create_task( asyncio.to_thread( - self.minio.fget_object, - self.bucket, - entry.ident, - target_file_video, + self.minio.fget_object, self.bucket, entry.ident, video_dl_path ) ) - await video_task - return target_file_video, None + if audio_path is not None: + audio_dl_path: Optional[str] = os.path.join(self.tmp_dir, audio_path) + + audio_dl_task: asyncio.Task[Any] = asyncio.create_task( + asyncio.to_thread( + self.minio.fget_object, self.bucket, audio_path, audio_dl_path + ) + ) + else: + audio_dl_path = None + audio_dl_task = asyncio.create_task(asyncio.sleep(0)) + + await video_dl_task + await audio_dl_task + + return video_dl_path, audio_dl_path available_sources["s3"] = S3Source diff --git a/syng/sources/source.py b/syng/sources/source.py index 6612ffc..2d3d734 100644 --- a/syng/sources/source.py +++ b/syng/sources/source.py @@ -19,6 +19,7 @@ from typing import Any from typing import Optional from typing import Tuple from typing import Type +from abc import ABC, abstractmethod from ..entry import Entry from ..result import Result @@ -65,7 +66,7 @@ class DLFilesEntry: buffer_task: Optional[asyncio.Task[Tuple[str, Optional[str]]]] = None -class Source: +class Source(ABC): """Parentclass for all sources. A new source should subclass this, and at least implement @@ -103,6 +104,11 @@ class Source: - ``source_name``, the string used to identify the source """ + source_name: str = "" + config_schema: dict[str, tuple[type | list[type], str, Any]] = { + "enabled": (bool, "Enable this source", False) + } + def __init__(self, config: dict[str, Any]): """ Create and initialize a new source. @@ -114,7 +120,6 @@ class Source: source for documentation. :type config: dict[str, Any] """ - self.source_name: str = "" self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict( DLFilesEntry ) @@ -144,6 +149,8 @@ class Source: [f"--audio-file={audio}"] if audio else [] ) + print(f"File is {video=} and {audio=}") + mpv_process = asyncio.create_subprocess_exec( "mpv", *args, @@ -207,6 +214,7 @@ class Source: results.append(result) return results + @abstractmethod async def do_buffer(self, entry: Entry) -> Tuple[str, Optional[str]]: """ Source specific part of buffering. @@ -223,7 +231,7 @@ class Source: :returns: A Tuple of the locations for the video and the audio file. :rtype: Tuple[str, Optional[str]] """ - raise NotImplementedError + ... async def buffer(self, entry: Entry) -> None: """ @@ -399,6 +407,7 @@ class Source: :return: The part of the config, that should be sended to the server. :rtype: dict[str, Any] | list[dict[str, Any]] """ + print("xzy") if not self._index: self._index = [] print(f"{self.source_name}: generating index") diff --git a/syng/sources/youtube.py b/syng/sources/youtube.py index 1e20a0c..0272512 100644 --- a/syng/sources/youtube.py +++ b/syng/sources/youtube.py @@ -50,12 +50,23 @@ class YoutubeSource(Source): ``yt-dlp``. Default is False. """ + source_name = "youtube" + config_schema = Source.config_schema | { + "channels": (list, "A list channels to search in", []), + "tmp_dir": (str, "Folder for temporary download", "/tmp/syng"), + "max_res": (int, "Maximum resolution to download", 720), + "start_streaming": ( + bool, + "Start streaming if download is not complete", + False, + ), + } + # pylint: disable=too-many-instance-attributes def __init__(self, config: dict[str, Any]): """Create the source.""" super().__init__(config) - self.source_name = "youtube" self.innertube_client: innertube.InnerTube = innertube.InnerTube(client="WEB") self.channels: list[str] = config["channels"] if "channels" in config else [] -- 2.45.3 From 4d130e05995e6400498bd9c7cfd813e052f26ade Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Tue, 7 Nov 2023 09:07:46 +0100 Subject: [PATCH 02/18] Reorganization of gui code --- syng/gui.py | 335 ++++++++++++++++++++++++++-------------------------- 1 file changed, 170 insertions(+), 165 deletions(-) diff --git a/syng/gui.py b/syng/gui.py index fceb4b6..6c36178 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -12,6 +12,147 @@ from syng.client import default_config from .sources import available_sources +class SourceTab(customtkinter.CTkFrame): + def updateStrVar(self, var: str, element: customtkinter.CTkTextbox, event): + value = element.get("0.0", "end").strip() + self.vars[var] = value + + def updateBoolVar(self, var: str, element: customtkinter.CTkCheckBox, event): + value = True if element.get() == 1 else False + self.vars[var] = value + + def updateListVar(self, var: str, element: customtkinter.CTkTextbox, event): + value = [v.strip() for v in element.get("0.0", "end").strip().split(",")] + self.vars[var] = value + + def __init__(self, parent, source_name, config): + super().__init__(parent) + source = available_sources[source_name] + self.vars: dict[str, str | bool | list[str]] = {} + for row, (name, (typ, desc, default)) in enumerate( + source.config_schema.items() + ): + value = config[name] if name in config else default + self.vars[name] = value + label = customtkinter.CTkLabel(self, text=f"{desc} ({name})") + label.grid(column=0, row=row) + match typ: + case builtins.bool: + checkbox = customtkinter.CTkCheckBox( + self, + text="", + onvalue=True, + offvalue=False, + ) + checkbox.bind( + "", partial(self.updateBoolVar, name, checkbox) + ) + checkbox.bind( + "", partial(self.updateBoolVar, name, checkbox) + ) + if value: + checkbox.select() + else: + checkbox.deselect() + checkbox.grid(column=1, row=row) + case builtins.list: + inputfield = customtkinter.CTkTextbox(self, wrap="none", height=1) + inputfield.bind( + "", partial(self.updateStrVar, name, inputfield) + ) + inputfield.bind( + "", partial(self.updateStrVar, name, inputfield) + ) + inputfield.insert("0.0", ", ".join(value)) + inputfield.grid(column=1, row=row) + case builtins.str: + inputfield = customtkinter.CTkTextbox(self, wrap="none", height=1) + inputfield.bind( + "", partial(self.updateStrVar, name, inputfield) + ) + inputfield.bind( + "", partial(self.updateStrVar, name, inputfield) + ) + inputfield.insert("0.0", value) + inputfield.grid(column=1, row=row) + + def get_config(self): + return self.vars + + +class GeneralConfig(customtkinter.CTkFrame): + def __init__(self, parent, config, callback): + super().__init__(parent) + customtkinter.CTkLabel(self, text="Server", justify="left").grid( + column=0, row=0, padx=5, pady=5 + ) + self.serverTextbox = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.serverTextbox.grid(column=1, row=0) + self.serverTextbox.bind("", callback) + + customtkinter.CTkLabel(self, text="Room", justify="left").grid(column=0, row=1) + self.roomTextbox = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.roomTextbox.grid(column=1, row=1) + self.roomTextbox.bind("", callback) + + customtkinter.CTkLabel(self, text="Secret", justify="left").grid( + column=0, row=3 + ) + self.secretTextbox = customtkinter.CTkTextbox(self, wrap="none", height=1) + + secret = "".join( + secrets.choice(string.ascii_letters + string.digits) for _ in range(8) + ) + + self.secretTextbox.grid(column=1, row=3) + + customtkinter.CTkLabel(self, text="Waiting room policy", justify="left").grid( + column=0, row=4 + ) + self.waitingRoomPolicy = customtkinter.CTkOptionMenu( + self, values=["forced", "optional", "none"] + ) + self.waitingRoomPolicy.set("none") + self.waitingRoomPolicy.grid(column=1, row=4) + + customtkinter.CTkLabel(self, text="Time of last Song", justify="left").grid( + column=0, row=5 + ) + self.last_song = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.last_song.grid(column=1, row=5) + + customtkinter.CTkLabel(self, text="Preview Duration", justify="left").grid( + column=0, row=6 + ) + self.preview_duration = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.preview_duration.grid(column=1, row=6) + + self.serverTextbox.insert("0.0", config["server"]) + self.roomTextbox.insert("0.0", config["room"]) + self.secretTextbox.insert( + "0.0", config["secret"] if "secret" in config else secret + ) + self.waitingRoomPolicy.set(str(config["waiting_room_policy"]).lower()) + if config["last_song"]: + self.last_song.insert("0.0", config["last_song"]) + self.preview_duration.insert("0.0", config["preview_duration"]) + + def get_config(self): + config = {} + config["server"] = self.serverTextbox.get("0.0", "end").strip() + config["room"] = self.roomTextbox.get("0.0", "end").strip() + config["secret"] = self.secretTextbox.get("0.0", "end").strip() + config["waiting_room_policy"] = self.waitingRoomPolicy.get().strip() + config["last_song"] = self.last_song.get("0.0", "end").strip() + try: + config["preview_duration"] = int( + self.preview_duration.get("0.0", "end").strip() + ) + except ValueError: + config["preview_duration"] = 0 + return config + + class SyngGui(customtkinter.CTk): def loadConfig(self): filedialog.askopenfilename() @@ -19,6 +160,12 @@ class SyngGui(customtkinter.CTk): def __init__(self): super().__init__(className="Syng") + with open("syng-client.json") as cfile: + loaded_config = load(cfile) + config = {"sources": {}, "config": {}} + if "config" in loaded_config: + config["config"] = default_config() | loaded_config["config"] + self.wm_title("Syng") tabview = customtkinter.CTkTabview(self) tabview.pack(side="top") @@ -38,185 +185,42 @@ class SyngGui(customtkinter.CTk): ) loadbutton.pack(side="left") + startbutton = customtkinter.CTkButton(self, text="Start", command=self.start) + startbutton.pack(side="right") + frm = customtkinter.CTkFrame(tabview.tab("General")) frm.grid(ipadx=10) self.qrlabel = customtkinter.CTkLabel(frm, text="") self.qrlabel.grid(column=0, row=0) - optionframe = customtkinter.CTkFrame(frm) - optionframe.grid(column=1, row=0) + self.general_config = GeneralConfig(frm, config["config"], self.updateQr) + self.general_config.grid(column=1, row=0) - customtkinter.CTkLabel(optionframe, text="Server", justify="left").grid( - column=0, row=0, padx=5, pady=5 - ) - self.serverTextbox = customtkinter.CTkTextbox( - optionframe, wrap="none", height=1 - ) - self.serverTextbox.grid(column=1, row=0) - self.serverTextbox.bind("", self.updateQr) + self.tabs = {} - customtkinter.CTkLabel(optionframe, text="Room", justify="left").grid( - column=0, row=1 - ) - self.roomTextbox = customtkinter.CTkTextbox(optionframe, wrap="none", height=1) - self.roomTextbox.grid(column=1, row=1) - self.roomTextbox.bind("", self.updateQr) + for source_name in available_sources: + try: + source_config = loaded_config["sources"][source_name] + except KeyError: + source_config = {} - customtkinter.CTkLabel(optionframe, text="Secret", justify="left").grid( - column=0, row=3 - ) - self.secretTextbox = customtkinter.CTkTextbox( - optionframe, wrap="none", height=1 - ) - secret = "".join( - secrets.choice(string.ascii_letters + string.digits) for _ in range(8) - ) - self.secretTextbox.grid(column=1, row=3) - - customtkinter.CTkLabel( - optionframe, text="Waiting room policy", justify="left" - ).grid(column=0, row=4) - self.waitingRoomPolicy = customtkinter.CTkOptionMenu( - optionframe, values=["forced", "optional", "none"] - ) - self.waitingRoomPolicy.set("none") - self.waitingRoomPolicy.grid(column=1, row=4) - - customtkinter.CTkLabel( - optionframe, text="Time of last Song", justify="left" - ).grid(column=0, row=5) - self.last_song = customtkinter.CTkTextbox(optionframe, wrap="none", height=1) - self.last_song.grid(column=1, row=5) - - customtkinter.CTkLabel( - optionframe, text="Preview Duration", justify="left" - ).grid(column=0, row=6) - self.preview_duration = customtkinter.CTkTextbox( - optionframe, wrap="none", height=1 - ) - self.preview_duration.grid(column=1, row=6) - - customtkinter.CTkButton(optionframe, text="Start", command=self.start).grid( - column=0, row=7, columnspan=2, pady=10 - ) - - with open("syng-client.json") as cfile: - loaded_config = load(cfile) - config = {"sources": {}, "config": {}} - - self.source_config_elements = {} - for source_name, source in available_sources.items(): - self.source_config_elements[source_name] = {} - config["sources"][source_name] = {} - sourcefrm = customtkinter.CTkFrame(tabview.tab(source_name)) - sourcefrm.grid(ipadx=10) - for row, (name, (typ, desc, default)) in enumerate( - source.config_schema.items() - ): - if name in loaded_config["sources"][source_name]: - config["sources"][source_name][name] = loaded_config["sources"][ - source_name - ][name] - else: - config["sources"][source_name][name] = default - - label = customtkinter.CTkLabel( - sourcefrm, text=f"{desc} ({name})", justify="right" - ) - label.grid(column=0, row=row) - match typ: - case builtins.bool: - self.source_config_elements[source_name][ - name - ] = customtkinter.CTkSwitch(sourcefrm, text="") - self.source_config_elements[source_name][name].grid( - column=1, row=row - ) - if config["sources"][source_name][name]: - self.source_config_elements[source_name][name].select() - else: - self.source_config_elements[source_name][name].deselect() - - case builtins.list: - self.source_config_elements[source_name][ - name - ] = customtkinter.CTkTextbox(sourcefrm, wrap="none", height=1) - self.source_config_elements[source_name][name].grid( - column=1, row=row - ) - self.source_config_elements[source_name][name].insert( - "0.0", ",".join(config["sources"][source_name][name]) - ) - - case _: - self.source_config_elements[source_name][ - name - ] = customtkinter.CTkTextbox(sourcefrm, wrap="none", height=1) - self.source_config_elements[source_name][name].grid( - column=1, row=row - ) - self.source_config_elements[source_name][name].insert( - "0.0", config["sources"][source_name][name] - ) - if source_name in loaded_config["sources"]: - config["sources"][source_name] |= loaded_config["sources"][source_name] - - if "config" in loaded_config: - config["config"] = default_config() | loaded_config["config"] - - self.serverTextbox.insert("0.0", config["config"]["server"]) - self.roomTextbox.insert("0.0", config["config"]["room"]) - self.secretTextbox.insert("0.0", config["config"]["secret"]) - self.waitingRoomPolicy.set(str(config["config"]["waiting_room_policy"]).lower()) - if config["config"]["last_song"]: - self.last_song.insert("0.0", config["config"]["last_song"]) - self.preview_duration.insert("0.0", config["config"]["preview_duration"]) + self.tabs[source_name] = SourceTab( + tabview.tab(source_name), source_name, source_config + ) + self.tabs[source_name].grid(ipadx=10) self.updateQr() def start(self): - config = {} - config["server"] = self.serverTextbox.get("0.0", "end").strip() - config["room"] = self.roomTextbox.get("0.0", "end").strip() - config["secret"] = self.secretTextbox.get("0.0", "end").strip() - config["waiting_room_policy"] = self.waitingRoomPolicy.get().strip() - config["last_song"] = self.last_song.get("0.0", "end").strip() - try: - config["preview_duration"] = int( - self.preview_duration.get("0.0", "end").strip() - ) - except ValueError: - config["preview_duration"] = 0 - sources = {} - for source_name, config_elements in self.source_config_elements.items(): - sources[source_name] = {} - for option, config_element in config_elements.items(): - if isinstance(config_element, customtkinter.CTkSwitch): - sources[source_name][option] = ( - True if config_element.get() == 1 else False - ) - elif isinstance(config_element, customtkinter.CTkTextbox): - match available_sources[source_name].config_schema[option][0]: - case builtins.list: - sources[source_name][option] = [ - value.strip() - for value in config_element.get("0.0", "end") - .strip() - .split(",") - ] + for source, tab in self.tabs.items(): + sources[source] = tab.get_config() - case builtins.str: - sources[source_name][option] = config_element.get( - "0.0", "end" - ).strip() - else: - raise RuntimeError("IDK") + general_config = self.general_config.get_config() - syng_config = {"sources": sources, "config": config} - - print(syng_config) + config = {"sources": sources, "config": general_config} + print(config) def changeQr(self, data: str): qr = qrcode.QRCode(box_size=20, border=2) @@ -228,9 +232,10 @@ class SyngGui(customtkinter.CTk): self.qrlabel.configure(image=tkQrcode) def updateQr(self, _evt=None): - server = self.serverTextbox.get("0.0", "end").strip() + config = self.general_config.get_config() + server = config["server"] server += "" if server.endswith("/") else "/" - room = self.roomTextbox.get("0.0", "end").strip() + room = config["room"] print(server + room) self.changeQr(server + room) -- 2.45.3 From e3a30d5f2bab00d545a13c2af6266ddf132a4f4d Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Tue, 7 Nov 2023 23:30:56 +0100 Subject: [PATCH 03/18] Add secret to default configs --- syng/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syng/client.py b/syng/client.py index de8143b..15310f6 100644 --- a/syng/client.py +++ b/syng/client.py @@ -66,6 +66,7 @@ currentLock: asyncio.Semaphore = asyncio.Semaphore(0) def default_config() -> dict[str, Optional[int | str]]: return { "preview_duration": 3, + "secret": None, "last_song": None, "waiting_room_policy": None, } -- 2.45.3 From 7a65785a9ec0a31073f11a82f562085ccf6cb388 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Tue, 7 Nov 2023 23:31:11 +0100 Subject: [PATCH 04/18] small steps towards readable gui code --- syng/gui.py | 114 +++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 60 deletions(-) diff --git a/syng/gui.py b/syng/gui.py index 6c36178..719879c 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -81,73 +81,60 @@ class SourceTab(customtkinter.CTkFrame): class GeneralConfig(customtkinter.CTkFrame): + def add_option_label(self, text): + customtkinter.CTkLabel(self, text=text, justify="left").grid( + column=0, row=self.number_of_options, padx=5, pady=5 + ) + + def add_string_option(self, name, description, callback=None): + self.add_option_label(description) + + self.string_options[name] = customtkinter.CTkTextbox( + self, wrap="none", height=1 + ) + self.string_options[name].grid(column=1, row=self.number_of_options) + if callback is not None: + self.string_options[name].bind("", callback) + self.number_of_options += 1 + + def add_choose_option(self, name, description, values): + self.add_option_label(description) + self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) + self.choose_options[name].grid(column=1, row=self.number_of_options) + self.number_of_options += 1 + def __init__(self, parent, config, callback): super().__init__(parent) - customtkinter.CTkLabel(self, text="Server", justify="left").grid( - column=0, row=0, padx=5, pady=5 - ) - self.serverTextbox = customtkinter.CTkTextbox(self, wrap="none", height=1) - self.serverTextbox.grid(column=1, row=0) - self.serverTextbox.bind("", callback) + self.number_of_options = 0 + self.string_options = {} + self.choose_options = {} - customtkinter.CTkLabel(self, text="Room", justify="left").grid(column=0, row=1) - self.roomTextbox = customtkinter.CTkTextbox(self, wrap="none", height=1) - self.roomTextbox.grid(column=1, row=1) - self.roomTextbox.bind("", callback) - - customtkinter.CTkLabel(self, text="Secret", justify="left").grid( - column=0, row=3 + self.add_string_option("server", "Server", callback) + self.add_string_option("room", "Room", callback) + self.add_string_option("secret", "Secret") + self.add_choose_option( + "waiting_room_policy", "Waiting room policy", ["forced", "optional", "none"] ) - self.secretTextbox = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.add_string_option("last_song", "Time of last song") + self.add_string_option("preview_duration", "Preview Duration") - secret = "".join( - secrets.choice(string.ascii_letters + string.digits) for _ in range(8) - ) + for name, textbox in self.string_options.items(): + if config[name]: + textbox.insert("0.0", config[name]) - self.secretTextbox.grid(column=1, row=3) - - customtkinter.CTkLabel(self, text="Waiting room policy", justify="left").grid( - column=0, row=4 - ) - self.waitingRoomPolicy = customtkinter.CTkOptionMenu( - self, values=["forced", "optional", "none"] - ) - self.waitingRoomPolicy.set("none") - self.waitingRoomPolicy.grid(column=1, row=4) - - customtkinter.CTkLabel(self, text="Time of last Song", justify="left").grid( - column=0, row=5 - ) - self.last_song = customtkinter.CTkTextbox(self, wrap="none", height=1) - self.last_song.grid(column=1, row=5) - - customtkinter.CTkLabel(self, text="Preview Duration", justify="left").grid( - column=0, row=6 - ) - self.preview_duration = customtkinter.CTkTextbox(self, wrap="none", height=1) - self.preview_duration.grid(column=1, row=6) - - self.serverTextbox.insert("0.0", config["server"]) - self.roomTextbox.insert("0.0", config["room"]) - self.secretTextbox.insert( - "0.0", config["secret"] if "secret" in config else secret - ) - self.waitingRoomPolicy.set(str(config["waiting_room_policy"]).lower()) - if config["last_song"]: - self.last_song.insert("0.0", config["last_song"]) - self.preview_duration.insert("0.0", config["preview_duration"]) + for name, optionmenu in self.choose_options.items(): + optionmenu.set(str(config[name]).lower()) def get_config(self): config = {} - config["server"] = self.serverTextbox.get("0.0", "end").strip() - config["room"] = self.roomTextbox.get("0.0", "end").strip() - config["secret"] = self.secretTextbox.get("0.0", "end").strip() - config["waiting_room_policy"] = self.waitingRoomPolicy.get().strip() - config["last_song"] = self.last_song.get("0.0", "end").strip() + for name, textbox in self.string_options.items(): + config[name] = textbox.get("0.0", "end").strip() + + for name, optionmenu in self.choose_options.items(): + config[name] = optionmenu.get().strip() + try: - config["preview_duration"] = int( - self.preview_duration.get("0.0", "end").strip() - ) + config["preview_duration"] = int(config["preview_duration"]) except ValueError: config["preview_duration"] = 0 return config @@ -162,9 +149,14 @@ class SyngGui(customtkinter.CTk): with open("syng-client.json") as cfile: loaded_config = load(cfile) - config = {"sources": {}, "config": {}} + config = {"sources": {}, "config": default_config()} if "config" in loaded_config: - config["config"] = default_config() | loaded_config["config"] + config["config"] |= loaded_config["config"] + + if not config["config"]["secret"]: + config["config"]["secret"] = "".join( + secrets.choice(string.ascii_letters + string.digits) for _ in range(8) + ) self.wm_title("Syng") tabview = customtkinter.CTkTabview(self) @@ -185,7 +177,9 @@ class SyngGui(customtkinter.CTk): ) loadbutton.pack(side="left") - startbutton = customtkinter.CTkButton(self, text="Start", command=self.start) + startbutton = customtkinter.CTkButton( + fileframe, text="Start", command=self.start + ) startbutton.pack(side="right") frm = customtkinter.CTkFrame(tabview.tab("General")) -- 2.45.3 From 9fdb9ef76dad4859d9dcce38d4bf06bb5f0c434c Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Thu, 9 Nov 2023 09:38:44 +0100 Subject: [PATCH 05/18] More unifying the gui code --- syng/gui.py | 210 ++++++++++++++++++++++++++++------------------------ 1 file changed, 115 insertions(+), 95 deletions(-) diff --git a/syng/gui.py b/syng/gui.py index 719879c..7656296 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,5 +1,4 @@ import builtins -from functools import partial from json import load import customtkinter import qrcode @@ -12,7 +11,88 @@ from syng.client import default_config from .sources import available_sources -class SourceTab(customtkinter.CTkFrame): +class OptionFrame(customtkinter.CTkFrame): + def add_option_label(self, text): + customtkinter.CTkLabel(self, text=text, justify="left").grid( + column=0, row=self.number_of_options, padx=5, pady=5 + ) + + def add_bool_option(self, name, description, value=False): + self.add_option_label(description) + self.bool_options[name] = customtkinter.CTkCheckBox( + self, + text="", + onvalue=True, + offvalue=False, + ) + if value: + self.bool_options[name].select() + else: + self.bool_options[name].deselect() + self.bool_options[name].grid(column=1, row=self.number_of_options) + self.number_of_options += 1 + + def add_string_option(self, name, description, value="", callback=None): + self.add_option_label(description) + if value is None: + value = "" + + self.string_options[name] = customtkinter.CTkTextbox( + self, wrap="none", height=1 + ) + self.string_options[name].grid(column=1, row=self.number_of_options) + self.string_options[name].insert("0.0", value) + if callback is not None: + self.string_options[name].bind("", callback) + self.string_options[name].bind("", callback) + self.number_of_options += 1 + + def add_list_option(self, name, description, value=[], callback=None): + self.add_option_label(description) + + self.list_options[name] = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.list_options[name].grid(column=1, row=self.number_of_options) + self.list_options[name].insert("0.0", ", ".join(value)) + if callback is not None: + self.list_options[name].bind("", callback) + self.list_options[name].bind("", callback) + self.number_of_options += 1 + + def add_choose_option(self, name, description, values, value=""): + self.add_option_label(description) + self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) + self.choose_options[name].grid(column=1, row=self.number_of_options) + self.choose_options[name].set(value) + self.number_of_options += 1 + + def __init__(self, parent): + super().__init__(parent) + self.number_of_options = 0 + self.string_options = {} + self.choose_options = {} + self.bool_options = {} + self.list_options = {} + + def get_config(self): + config = {} + for name, textbox in self.string_options.items(): + config[name] = textbox.get("0.0", "end").strip() + + for name, optionmenu in self.choose_options.items(): + config[name] = optionmenu.get().strip() + + for name, checkbox in self.bool_options.items(): + config[name] = checkbox.get() == 1 + + for name, textbox in self.list_options.items(): + config[name] = [ + v.strip() for v in textbox.get("0.0", "end").strip().split(",") + ] + + return config + + +class SourceTab(OptionFrame): def updateStrVar(self, var: str, element: customtkinter.CTkTextbox, event): value = element.get("0.0", "end").strip() self.vars[var] = value @@ -33,90 +113,32 @@ class SourceTab(customtkinter.CTkFrame): source.config_schema.items() ): value = config[name] if name in config else default - self.vars[name] = value - label = customtkinter.CTkLabel(self, text=f"{desc} ({name})") - label.grid(column=0, row=row) match typ: case builtins.bool: - checkbox = customtkinter.CTkCheckBox( - self, - text="", - onvalue=True, - offvalue=False, - ) - checkbox.bind( - "", partial(self.updateBoolVar, name, checkbox) - ) - checkbox.bind( - "", partial(self.updateBoolVar, name, checkbox) - ) - if value: - checkbox.select() - else: - checkbox.deselect() - checkbox.grid(column=1, row=row) + self.add_bool_option(name, desc, value=value) case builtins.list: - inputfield = customtkinter.CTkTextbox(self, wrap="none", height=1) - inputfield.bind( - "", partial(self.updateStrVar, name, inputfield) - ) - inputfield.bind( - "", partial(self.updateStrVar, name, inputfield) - ) - inputfield.insert("0.0", ", ".join(value)) - inputfield.grid(column=1, row=row) + self.add_list_option(name, desc, value=value) case builtins.str: - inputfield = customtkinter.CTkTextbox(self, wrap="none", height=1) - inputfield.bind( - "", partial(self.updateStrVar, name, inputfield) - ) - inputfield.bind( - "", partial(self.updateStrVar, name, inputfield) - ) - inputfield.insert("0.0", value) - inputfield.grid(column=1, row=row) - - def get_config(self): - return self.vars + self.add_string_option(name, desc, value=value) -class GeneralConfig(customtkinter.CTkFrame): - def add_option_label(self, text): - customtkinter.CTkLabel(self, text=text, justify="left").grid( - column=0, row=self.number_of_options, padx=5, pady=5 - ) - - def add_string_option(self, name, description, callback=None): - self.add_option_label(description) - - self.string_options[name] = customtkinter.CTkTextbox( - self, wrap="none", height=1 - ) - self.string_options[name].grid(column=1, row=self.number_of_options) - if callback is not None: - self.string_options[name].bind("", callback) - self.number_of_options += 1 - - def add_choose_option(self, name, description, values): - self.add_option_label(description) - self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) - self.choose_options[name].grid(column=1, row=self.number_of_options) - self.number_of_options += 1 - +class GeneralConfig(OptionFrame): def __init__(self, parent, config, callback): super().__init__(parent) - self.number_of_options = 0 - self.string_options = {} - self.choose_options = {} - self.add_string_option("server", "Server", callback) - self.add_string_option("room", "Room", callback) - self.add_string_option("secret", "Secret") + self.add_string_option("server", "Server", config["server"], callback) + self.add_string_option("room", "Room", config["room"], callback) + self.add_string_option("secret", "Secret", config["secret"]) self.add_choose_option( - "waiting_room_policy", "Waiting room policy", ["forced", "optional", "none"] + "waiting_room_policy", + "Waiting room policy", + ["forced", "optional", "none"], + config["waiting_room_policy"], + ) + self.add_string_option("last_song", "Time of last song", config["last_song"]) + self.add_string_option( + "preview_duration", "Preview Duration", config["preview_duration"] ) - self.add_string_option("last_song", "Time of last song") - self.add_string_option("preview_duration", "Preview Duration") for name, textbox in self.string_options.items(): if config[name]: @@ -126,17 +148,12 @@ class GeneralConfig(customtkinter.CTkFrame): optionmenu.set(str(config[name]).lower()) def get_config(self): - config = {} - for name, textbox in self.string_options.items(): - config[name] = textbox.get("0.0", "end").strip() - - for name, optionmenu in self.choose_options.items(): - config[name] = optionmenu.get().strip() - + config = super().get_config() try: config["preview_duration"] = int(config["preview_duration"]) except ValueError: config["preview_duration"] = 0 + return config @@ -159,13 +176,6 @@ class SyngGui(customtkinter.CTk): ) self.wm_title("Syng") - tabview = customtkinter.CTkTabview(self) - tabview.pack(side="top") - - tabview.add("General") - for source in available_sources: - tabview.add(source) - tabview.set("General") fileframe = customtkinter.CTkFrame(self) fileframe.pack(side="bottom") @@ -182,14 +192,24 @@ class SyngGui(customtkinter.CTk): ) startbutton.pack(side="right") - frm = customtkinter.CTkFrame(tabview.tab("General")) - frm.grid(ipadx=10) + frm = customtkinter.CTkFrame(self) + frm.pack(ipadx=10, padx=10, fill="both", expand=True) + + tabview = customtkinter.CTkTabview(frm) + tabview.pack(side="right", padx=10, pady=10, fill="both", expand=True) + + tabview.add("General") + for source in available_sources: + tabview.add(source) + tabview.set("General") self.qrlabel = customtkinter.CTkLabel(frm, text="") - self.qrlabel.grid(column=0, row=0) + self.qrlabel.pack(side="left") - self.general_config = GeneralConfig(frm, config["config"], self.updateQr) - self.general_config.grid(column=1, row=0) + self.general_config = GeneralConfig( + tabview.tab("General"), config["config"], self.updateQr + ) + self.general_config.pack(ipadx=10, fill="y") self.tabs = {} @@ -202,7 +222,7 @@ class SyngGui(customtkinter.CTk): self.tabs[source_name] = SourceTab( tabview.tab(source_name), source_name, source_config ) - self.tabs[source_name].grid(ipadx=10) + self.tabs[source_name].pack(ipadx=10) self.updateQr() -- 2.45.3 From 3d8f0f5f3c85014532fea9507c9587d28f0a53f0 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Sun, 12 Nov 2023 10:09:11 +0100 Subject: [PATCH 06/18] Updated compiled web ui --- syng/PIL.pyi | 0 syng/static/assets/index.20e81f9f.js | 787 ++++++++++++++++++++++++++ syng/static/assets/index.8572a105.js | 787 -------------------------- syng/static/assets/index.9110b89b.css | 1 - syng/static/assets/index.b030f504.css | 1 + 5 files changed, 788 insertions(+), 788 deletions(-) delete mode 100644 syng/PIL.pyi create mode 100644 syng/static/assets/index.20e81f9f.js delete mode 100644 syng/static/assets/index.8572a105.js delete mode 100644 syng/static/assets/index.9110b89b.css create mode 100644 syng/static/assets/index.b030f504.css diff --git a/syng/PIL.pyi b/syng/PIL.pyi deleted file mode 100644 index e69de29..0000000 diff --git a/syng/static/assets/index.20e81f9f.js b/syng/static/assets/index.20e81f9f.js new file mode 100644 index 0000000..7a645f3 --- /dev/null +++ b/syng/static/assets/index.20e81f9f.js @@ -0,0 +1,787 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerpolicy&&(s.referrerPolicy=i.referrerpolicy),i.crossorigin==="use-credentials"?s.credentials="include":i.crossorigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();function Qo(e,t){const n=Object.create(null),r=e.split(",");for(let i=0;i!!n[i.toLowerCase()]:i=>!!n[i]}function Go(e){if(Ee(e)){const t={};for(let n=0;n{if(n){const r=n.split(Np);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function Xi(e){let t="";if(xt(e))t=e;else if(Ee(e))for(let n=0;nxt(e)?e:e==null?"":Ee(e)||at(e)&&(e.toString===vf||!Se(e.toString))?JSON.stringify(e,pf,2):String(e),pf=(e,t)=>t&&t.__v_isRef?pf(e,t.value):Yi(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,i])=>(n[`${r} =>`]=i,n),{})}:mf(t)?{[`Set(${t.size})`]:[...t.values()]}:at(t)&&!Ee(t)&&!yf(t)?String(t):t,tt={},Vi=[],gn=()=>{},qp=()=>!1,Bp=/^on[^a-z]/,ta=e=>Bp.test(e),Xo=e=>e.startsWith("onUpdate:"),Nt=Object.assign,Jo=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},jp=Object.prototype.hasOwnProperty,Me=(e,t)=>jp.call(e,t),Ee=Array.isArray,Yi=e=>na(e)==="[object Map]",mf=e=>na(e)==="[object Set]",Se=e=>typeof e=="function",xt=e=>typeof e=="string",Zo=e=>typeof e=="symbol",at=e=>e!==null&&typeof e=="object",gf=e=>at(e)&&Se(e.then)&&Se(e.catch),vf=Object.prototype.toString,na=e=>vf.call(e),Up=e=>na(e).slice(8,-1),yf=e=>na(e)==="[object Object]",el=e=>xt(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Rs=Qo(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ia=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Wp=/-(\w)/g,On=ia(e=>e.replace(Wp,(t,n)=>n?n.toUpperCase():"")),Vp=/\B([A-Z])/g,Ni=ia(e=>e.replace(Vp,"-$1").toLowerCase()),ra=ia(e=>e.charAt(0).toUpperCase()+e.slice(1)),Va=ia(e=>e?`on${ra(e)}`:""),Nr=(e,t)=>!Object.is(e,t),Ya=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},bf=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let Tu;const Yp=()=>Tu||(Tu=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});let Cn;class Kp{constructor(t=!1){this.detached=t,this.active=!0,this.effects=[],this.cleanups=[],this.parent=Cn,!t&&Cn&&(this.index=(Cn.scopes||(Cn.scopes=[])).push(this)-1)}run(t){if(this.active){const n=Cn;try{return Cn=this,t()}finally{Cn=n}}}on(){Cn=this}off(){Cn=this.parent}stop(t){if(this.active){let n,r;for(n=0,r=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},_f=e=>(e.w&oi)>0,wf=e=>(e.n&oi)>0,Gp=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let r=0;r{(m==="length"||m>=c)&&u.push(p)})}else switch(n!==void 0&&u.push(o.get(n)),t){case"add":Ee(e)?el(n)&&u.push(o.get("length")):(u.push(o.get(Si)),Yi(e)&&u.push(o.get(mo)));break;case"delete":Ee(e)||(u.push(o.get(Si)),Yi(e)&&u.push(o.get(mo)));break;case"set":Yi(e)&&u.push(o.get(Si));break}if(u.length===1)u[0]&&go(u[0]);else{const c=[];for(const p of u)p&&c.push(...p);go(tl(c))}}function go(e,t){const n=Ee(e)?e:[...e];for(const r of n)r.computed&&Eu(r);for(const r of n)r.computed||Eu(r)}function Eu(e,t){(e!==dn||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}const Jp=Qo("__proto__,__v_isRef,__isVue"),xf=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Zo)),Zp=il(),em=il(!1,!0),tm=il(!0),Su=nm();function nm(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=Ue(this);for(let s=0,o=this.length;s{e[t]=function(...n){sr();const r=Ue(this)[t].apply(this,n);return ar(),r}}),e}function il(e=!1,t=!1){return function(r,i,s){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_isShallow")return t;if(i==="__v_raw"&&s===(e?t?ym:Sf:t?Ef:Af).get(r))return r;const o=Ee(r);if(!e&&o&&Me(Su,i))return Reflect.get(Su,i,s);const u=Reflect.get(r,i,s);return(Zo(i)?xf.has(i):Jp(i))||(e||Zt(r,"get",i),t)?u:Lt(u)?o&&el(i)?u:u.value:at(u)?e?Of(u):or(u):u}}const im=$f(),rm=$f(!0);function $f(e=!1){return function(n,r,i,s){let o=n[r];if(Ji(o)&&Lt(o)&&!Lt(i))return!1;if(!e&&(!Bs(i)&&!Ji(i)&&(o=Ue(o),i=Ue(i)),!Ee(n)&&Lt(o)&&!Lt(i)))return o.value=i,!0;const u=Ee(n)&&el(r)?Number(r)e,sa=e=>Reflect.getPrototypeOf(e);function hs(e,t,n=!1,r=!1){e=e.__v_raw;const i=Ue(e),s=Ue(t);n||(t!==s&&Zt(i,"get",t),Zt(i,"get",s));const{has:o}=sa(i),u=r?rl:n?ol:Mr;if(o.call(i,t))return u(e.get(t));if(o.call(i,s))return u(e.get(s));e!==i&&e.get(t)}function ps(e,t=!1){const n=this.__v_raw,r=Ue(n),i=Ue(e);return t||(e!==i&&Zt(r,"has",e),Zt(r,"has",i)),e===i?n.has(e):n.has(e)||n.has(i)}function ms(e,t=!1){return e=e.__v_raw,!t&&Zt(Ue(e),"iterate",Si),Reflect.get(e,"size",e)}function Ou(e){e=Ue(e);const t=Ue(this);return sa(t).has.call(t,e)||(t.add(e),Hn(t,"add",e,e)),this}function Ru(e,t){t=Ue(t);const n=Ue(this),{has:r,get:i}=sa(n);let s=r.call(n,e);s||(e=Ue(e),s=r.call(n,e));const o=i.call(n,e);return n.set(e,t),s?Nr(t,o)&&Hn(n,"set",e,t):Hn(n,"add",e,t),this}function zu(e){const t=Ue(this),{has:n,get:r}=sa(t);let i=n.call(t,e);i||(e=Ue(e),i=n.call(t,e)),r&&r.call(t,e);const s=t.delete(e);return i&&Hn(t,"delete",e,void 0),s}function Pu(){const e=Ue(this),t=e.size!==0,n=e.clear();return t&&Hn(e,"clear",void 0,void 0),n}function gs(e,t){return function(r,i){const s=this,o=s.__v_raw,u=Ue(o),c=t?rl:e?ol:Mr;return!e&&Zt(u,"iterate",Si),o.forEach((p,m)=>r.call(i,c(p),c(m),s))}}function vs(e,t,n){return function(...r){const i=this.__v_raw,s=Ue(i),o=Yi(s),u=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,p=i[e](...r),m=n?rl:t?ol:Mr;return!t&&Zt(s,"iterate",c?mo:Si),{next(){const{value:w,done:C}=p.next();return C?{value:w,done:C}:{value:u?[m(w[0]),m(w[1])]:m(w),done:C}},[Symbol.iterator](){return this}}}}function Kn(e){return function(...t){return e==="delete"?!1:this}}function cm(){const e={get(s){return hs(this,s)},get size(){return ms(this)},has:ps,add:Ou,set:Ru,delete:zu,clear:Pu,forEach:gs(!1,!1)},t={get(s){return hs(this,s,!1,!0)},get size(){return ms(this)},has:ps,add:Ou,set:Ru,delete:zu,clear:Pu,forEach:gs(!1,!0)},n={get(s){return hs(this,s,!0)},get size(){return ms(this,!0)},has(s){return ps.call(this,s,!0)},add:Kn("add"),set:Kn("set"),delete:Kn("delete"),clear:Kn("clear"),forEach:gs(!0,!1)},r={get(s){return hs(this,s,!0,!0)},get size(){return ms(this,!0)},has(s){return ps.call(this,s,!0)},add:Kn("add"),set:Kn("set"),delete:Kn("delete"),clear:Kn("clear"),forEach:gs(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(s=>{e[s]=vs(s,!1,!1),n[s]=vs(s,!0,!1),t[s]=vs(s,!1,!0),r[s]=vs(s,!0,!0)}),[e,n,t,r]}const[fm,dm,hm,pm]=cm();function sl(e,t){const n=t?e?pm:hm:e?dm:fm;return(r,i,s)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?r:Reflect.get(Me(n,i)&&i in r?n:r,i,s)}const mm={get:sl(!1,!1)},gm={get:sl(!1,!0)},vm={get:sl(!0,!1)},Af=new WeakMap,Ef=new WeakMap,Sf=new WeakMap,ym=new WeakMap;function bm(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function _m(e){return e.__v_skip||!Object.isExtensible(e)?0:bm(Up(e))}function or(e){return Ji(e)?e:al(e,!1,Tf,mm,Af)}function wm(e){return al(e,!1,um,gm,Ef)}function Of(e){return al(e,!0,lm,vm,Sf)}function al(e,t,n,r,i){if(!at(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const s=i.get(e);if(s)return s;const o=_m(e);if(o===0)return e;const u=new Proxy(e,o===2?r:n);return i.set(e,u),u}function Ki(e){return Ji(e)?Ki(e.__v_raw):!!(e&&e.__v_isReactive)}function Ji(e){return!!(e&&e.__v_isReadonly)}function Bs(e){return!!(e&&e.__v_isShallow)}function Rf(e){return Ki(e)||Ji(e)}function Ue(e){const t=e&&e.__v_raw;return t?Ue(t):e}function zf(e){return qs(e,"__v_skip",!0),e}const Mr=e=>at(e)?or(e):e,ol=e=>at(e)?Of(e):e;function Pf(e){ii&&dn&&(e=Ue(e),Cf(e.dep||(e.dep=tl())))}function Lf(e,t){e=Ue(e),e.dep&&go(e.dep)}function Lt(e){return!!(e&&e.__v_isRef===!0)}function ll(e){return If(e,!1)}function km(e){return If(e,!0)}function If(e,t){return Lt(e)?e:new Cm(e,t)}class Cm{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:Ue(t),this._value=n?t:Mr(t)}get value(){return Pf(this),this._value}set value(t){const n=this.__v_isShallow||Bs(t)||Ji(t);t=n?t:Ue(t),Nr(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:Mr(t),Lf(this))}}function ri(e){return Lt(e)?e.value:e}const xm={get:(e,t,n)=>ri(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const i=e[t];return Lt(i)&&!Lt(n)?(i.value=n,!0):Reflect.set(e,t,n,r)}};function Nf(e){return Ki(e)?e:new Proxy(e,xm)}var Mf;class $m{constructor(t,n,r,i){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this[Mf]=!1,this._dirty=!0,this.effect=new nl(t,()=>{this._dirty||(this._dirty=!0,Lf(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!i,this.__v_isReadonly=r}get value(){const t=Ue(this);return Pf(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}Mf="__v_isReadonly";function Tm(e,t,n=!1){let r,i;const s=Se(e);return s?(r=e,i=gn):(r=e.get,i=e.set),new $m(r,i,s||!i,n)}function si(e,t,n,r){let i;try{i=r?e(...r):e()}catch(s){aa(s,t,n)}return i}function vn(e,t,n,r){if(Se(e)){const s=si(e,t,n,r);return s&&gf(s)&&s.catch(o=>{aa(o,t,n)}),s}const i=[];for(let s=0;s>>1;Hr(Pt[r])$n&&Pt.splice(t,1)}function Om(e){Ee(e)?Qi.push(...e):(!Dn||!Dn.includes(e,e.allowRecurse?xi+1:xi))&&Qi.push(e),Ff()}function Lu(e,t=Dr?$n+1:0){for(;tHr(n)-Hr(r)),xi=0;xie.id==null?1/0:e.id,Rm=(e,t)=>{const n=Hr(e)-Hr(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Bf(e){vo=!1,Dr=!0,Pt.sort(Rm);const t=gn;try{for($n=0;$nxt(O)?O.trim():O)),w&&(i=n.map(bf))}let u,c=r[u=Va(t)]||r[u=Va(On(t))];!c&&s&&(c=r[u=Va(Ni(t))]),c&&vn(c,e,6,i);const p=r[u+"Once"];if(p){if(!e.emitted)e.emitted={};else if(e.emitted[u])return;e.emitted[u]=!0,vn(p,e,6,i)}}function jf(e,t,n=!1){const r=t.emitsCache,i=r.get(e);if(i!==void 0)return i;const s=e.emits;let o={},u=!1;if(!Se(e)){const c=p=>{const m=jf(p,t,!0);m&&(u=!0,Nt(o,m))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!s&&!u?(at(e)&&r.set(e,null),null):(Ee(s)?s.forEach(c=>o[c]=null):Nt(o,s),at(e)&&r.set(e,o),o)}function oa(e,t){return!e||!ta(t)?!1:(t=t.slice(2).replace(/Once$/,""),Me(e,t[0].toLowerCase()+t.slice(1))||Me(e,Ni(t))||Me(e,t))}let rn=null,la=null;function js(e){const t=rn;return rn=e,la=e&&e.type.__scopeId||null,t}function fi(e){la=e}function di(){la=null}function Pm(e,t=rn,n){if(!t||e._n)return e;const r=(...i)=>{r._d&&ju(-1);const s=js(t);let o;try{o=e(...i)}finally{js(s),r._d&&ju(1)}return o};return r._n=!0,r._c=!0,r._d=!0,r}function Ka(e){const{type:t,vnode:n,proxy:r,withProxy:i,props:s,propsOptions:[o],slots:u,attrs:c,emit:p,render:m,renderCache:w,data:C,setupState:O,ctx:N,inheritAttrs:D}=e;let ee,R;const j=js(e);try{if(n.shapeFlag&4){const ae=i||r;ee=xn(m.call(ae,ae,w,s,O,C,N)),R=c}else{const ae=t;ee=xn(ae.length>1?ae(s,{attrs:c,slots:u,emit:p}):ae(s,null)),R=t.props?c:Lm(c)}}catch(ae){Or.length=0,aa(ae,e,1),ee=Ae(Ri)}let K=ee;if(R&&D!==!1){const ae=Object.keys(R),{shapeFlag:ke}=K;ae.length&&ke&7&&(o&&ae.some(Xo)&&(R=Im(R,o)),K=Zi(K,R))}return n.dirs&&(K=Zi(K),K.dirs=K.dirs?K.dirs.concat(n.dirs):n.dirs),n.transition&&(K.transition=n.transition),ee=K,js(j),ee}const Lm=e=>{let t;for(const n in e)(n==="class"||n==="style"||ta(n))&&((t||(t={}))[n]=e[n]);return t},Im=(e,t)=>{const n={};for(const r in e)(!Xo(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function Nm(e,t,n){const{props:r,children:i,component:s}=e,{props:o,children:u,patchFlag:c}=t,p=s.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return r?Iu(r,o,p):!!o;if(c&8){const m=t.dynamicProps;for(let w=0;we.__isSuspense;function Hm(e,t){t&&t.pendingBranch?Ee(e)?t.effects.push(...e):t.effects.push(e):Om(e)}function zs(e,t){if(Et){let n=Et.provides;const r=Et.parent&&Et.parent.provides;r===n&&(n=Et.provides=Object.create(r)),n[e]=t}}function yn(e,t,n=!1){const r=Et||rn;if(r){const i=r.parent==null?r.vnode.appContext&&r.vnode.appContext.provides:r.parent.provides;if(i&&e in i)return i[e];if(arguments.length>1)return n&&Se(t)?t.call(r.proxy):t}}const ys={};function Er(e,t,n){return Uf(e,t,n)}function Uf(e,t,{immediate:n,deep:r,flush:i,onTrack:s,onTrigger:o}=tt){const u=Et;let c,p=!1,m=!1;if(Lt(e)?(c=()=>e.value,p=Bs(e)):Ki(e)?(c=()=>e,r=!0):Ee(e)?(m=!0,p=e.some(K=>Ki(K)||Bs(K)),c=()=>e.map(K=>{if(Lt(K))return K.value;if(Ki(K))return Ti(K);if(Se(K))return si(K,u,2)})):Se(e)?t?c=()=>si(e,u,2):c=()=>{if(!(u&&u.isUnmounted))return w&&w(),vn(e,u,3,[C])}:c=gn,t&&r){const K=c;c=()=>Ti(K())}let w,C=K=>{w=R.onStop=()=>{si(K,u,4)}},O;if(Br)if(C=gn,t?n&&vn(t,u,3,[c(),m?[]:void 0,C]):c(),i==="sync"){const K=Og();O=K.__watcherHandles||(K.__watcherHandles=[])}else return gn;let N=m?new Array(e.length).fill(ys):ys;const D=()=>{if(!!R.active)if(t){const K=R.run();(r||p||(m?K.some((ae,ke)=>Nr(ae,N[ke])):Nr(K,N)))&&(w&&w(),vn(t,u,3,[K,N===ys?void 0:m&&N[0]===ys?[]:N,C]),N=K)}else R.run()};D.allowRecurse=!!t;let ee;i==="sync"?ee=D:i==="post"?ee=()=>Ut(D,u&&u.suspense):(D.pre=!0,u&&(D.id=u.uid),ee=()=>cl(D));const R=new nl(c,ee);t?n?D():N=R.run():i==="post"?Ut(R.run.bind(R),u&&u.suspense):R.run();const j=()=>{R.stop(),u&&u.scope&&Jo(u.scope.effects,R)};return O&&O.push(j),j}function Fm(e,t,n){const r=this.proxy,i=xt(e)?e.includes(".")?Wf(r,e):()=>r[e]:e.bind(r,r);let s;Se(t)?s=t:(s=t.handler,n=t);const o=Et;er(this);const u=Uf(i,s.bind(r),n);return o?er(o):Oi(),u}function Wf(e,t){const n=t.split(".");return()=>{let r=e;for(let i=0;i{Ti(n,t)});else if(yf(e))for(const n in e)Ti(e[n],t);return e}function Xr(e){return Se(e)?{setup:e,name:e.name}:e}const Ps=e=>!!e.type.__asyncLoader,Vf=e=>e.type.__isKeepAlive;function qm(e,t){Yf(e,"a",t)}function Bm(e,t){Yf(e,"da",t)}function Yf(e,t,n=Et){const r=e.__wdc||(e.__wdc=()=>{let i=n;for(;i;){if(i.isDeactivated)return;i=i.parent}return e()});if(ua(t,r,n),n){let i=n.parent;for(;i&&i.parent;)Vf(i.parent.vnode)&&jm(r,t,n,i),i=i.parent}}function jm(e,t,n,r){const i=ua(t,e,r,!0);Qf(()=>{Jo(r[t],i)},n)}function ua(e,t,n=Et,r=!1){if(n){const i=n[e]||(n[e]=[]),s=t.__weh||(t.__weh=(...o)=>{if(n.isUnmounted)return;sr(),er(n);const u=vn(t,n,e,o);return Oi(),ar(),u});return r?i.unshift(s):i.push(s),s}}const jn=e=>(t,n=Et)=>(!Br||e==="sp")&&ua(e,(...r)=>t(...r),n),Um=jn("bm"),ca=jn("m"),Wm=jn("bu"),Vm=jn("u"),Kf=jn("bum"),Qf=jn("um"),Ym=jn("sp"),Km=jn("rtg"),Qm=jn("rtc");function Gm(e,t=Et){ua("ec",e,t)}function Fr(e,t){const n=rn;if(n===null)return e;const r=ha(n)||n.proxy,i=e.dirs||(e.dirs=[]);for(let s=0;st(o,u,void 0,s&&s[u]));else{const o=Object.keys(e);i=new Array(o.length);for(let u=0,c=o.length;ue?od(e)?ha(e)||e.proxy:yo(e.parent):null,Sr=Nt(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>yo(e.parent),$root:e=>yo(e.root),$emit:e=>e.emit,$options:e=>fl(e),$forceUpdate:e=>e.f||(e.f=()=>cl(e.update)),$nextTick:e=>e.n||(e.n=Hf.bind(e.proxy)),$watch:e=>Fm.bind(e)}),Qa=(e,t)=>e!==tt&&!e.__isScriptSetup&&Me(e,t),Zm={get({_:e},t){const{ctx:n,setupState:r,data:i,props:s,accessCache:o,type:u,appContext:c}=e;let p;if(t[0]!=="$"){const O=o[t];if(O!==void 0)switch(O){case 1:return r[t];case 2:return i[t];case 4:return n[t];case 3:return s[t]}else{if(Qa(r,t))return o[t]=1,r[t];if(i!==tt&&Me(i,t))return o[t]=2,i[t];if((p=e.propsOptions[0])&&Me(p,t))return o[t]=3,s[t];if(n!==tt&&Me(n,t))return o[t]=4,n[t];bo&&(o[t]=0)}}const m=Sr[t];let w,C;if(m)return t==="$attrs"&&Zt(e,"get",t),m(e);if((w=u.__cssModules)&&(w=w[t]))return w;if(n!==tt&&Me(n,t))return o[t]=4,n[t];if(C=c.config.globalProperties,Me(C,t))return C[t]},set({_:e},t,n){const{data:r,setupState:i,ctx:s}=e;return Qa(i,t)?(i[t]=n,!0):r!==tt&&Me(r,t)?(r[t]=n,!0):Me(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(s[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:i,propsOptions:s}},o){let u;return!!n[o]||e!==tt&&Me(e,o)||Qa(t,o)||(u=s[0])&&Me(u,o)||Me(r,o)||Me(Sr,o)||Me(i.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Me(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};let bo=!0;function eg(e){const t=fl(e),n=e.proxy,r=e.ctx;bo=!1,t.beforeCreate&&Mu(t.beforeCreate,e,"bc");const{data:i,computed:s,methods:o,watch:u,provide:c,inject:p,created:m,beforeMount:w,mounted:C,beforeUpdate:O,updated:N,activated:D,deactivated:ee,beforeDestroy:R,beforeUnmount:j,destroyed:K,unmounted:ae,render:ke,renderTracked:h,renderTriggered:He,errorCaptured:Xe,serverPrefetch:Ze,expose:Tt,inheritAttrs:Mt,components:Z,directives:qe,filters:dt}=t;if(p&&tg(p,r,null,e.appContext.config.unwrapInjectedRef),o)for(const Ve in o){const Be=o[Ve];Se(Be)&&(r[Ve]=Be.bind(n))}if(i){const Ve=i.call(n,n);at(Ve)&&(e.data=or(Ve))}if(bo=!0,s)for(const Ve in s){const Be=s[Ve],Wt=Se(Be)?Be.bind(n,n):Se(Be.get)?Be.get.bind(n,n):gn,wn=!Se(Be)&&Se(Be.set)?Be.set.bind(n):gn,pt=st({get:Wt,set:wn});Object.defineProperty(r,Ve,{enumerable:!0,configurable:!0,get:()=>pt.value,set:Ot=>pt.value=Ot})}if(u)for(const Ve in u)Xf(u[Ve],r,n,Ve);if(c){const Ve=Se(c)?c.call(n):c;Reflect.ownKeys(Ve).forEach(Be=>{zs(Be,Ve[Be])})}m&&Mu(m,e,"c");function ht(Ve,Be){Ee(Be)?Be.forEach(Wt=>Ve(Wt.bind(n))):Be&&Ve(Be.bind(n))}if(ht(Um,w),ht(ca,C),ht(Wm,O),ht(Vm,N),ht(qm,D),ht(Bm,ee),ht(Gm,Xe),ht(Qm,h),ht(Km,He),ht(Kf,j),ht(Qf,ae),ht(Ym,Ze),Ee(Tt))if(Tt.length){const Ve=e.exposed||(e.exposed={});Tt.forEach(Be=>{Object.defineProperty(Ve,Be,{get:()=>n[Be],set:Wt=>n[Be]=Wt})})}else e.exposed||(e.exposed={});ke&&e.render===gn&&(e.render=ke),Mt!=null&&(e.inheritAttrs=Mt),Z&&(e.components=Z),qe&&(e.directives=qe)}function tg(e,t,n=gn,r=!1){Ee(e)&&(e=_o(e));for(const i in e){const s=e[i];let o;at(s)?"default"in s?o=yn(s.from||i,s.default,!0):o=yn(s.from||i):o=yn(s),Lt(o)&&r?Object.defineProperty(t,i,{enumerable:!0,configurable:!0,get:()=>o.value,set:u=>o.value=u}):t[i]=o}}function Mu(e,t,n){vn(Ee(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function Xf(e,t,n,r){const i=r.includes(".")?Wf(n,r):()=>n[r];if(xt(e)){const s=t[e];Se(s)&&Er(i,s)}else if(Se(e))Er(i,e.bind(n));else if(at(e))if(Ee(e))e.forEach(s=>Xf(s,t,n,r));else{const s=Se(e.handler)?e.handler.bind(n):t[e.handler];Se(s)&&Er(i,s,e)}}function fl(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:i,optionsCache:s,config:{optionMergeStrategies:o}}=e.appContext,u=s.get(t);let c;return u?c=u:!i.length&&!n&&!r?c=t:(c={},i.length&&i.forEach(p=>Ws(c,p,o,!0)),Ws(c,t,o)),at(t)&&s.set(t,c),c}function Ws(e,t,n,r=!1){const{mixins:i,extends:s}=t;s&&Ws(e,s,n,!0),i&&i.forEach(o=>Ws(e,o,n,!0));for(const o in t)if(!(r&&o==="expose")){const u=ng[o]||n&&n[o];e[o]=u?u(e[o],t[o]):t[o]}return e}const ng={data:Du,props:Ci,emits:Ci,methods:Ci,computed:Ci,beforeCreate:It,created:It,beforeMount:It,mounted:It,beforeUpdate:It,updated:It,beforeDestroy:It,beforeUnmount:It,destroyed:It,unmounted:It,activated:It,deactivated:It,errorCaptured:It,serverPrefetch:It,components:Ci,directives:Ci,watch:rg,provide:Du,inject:ig};function Du(e,t){return t?e?function(){return Nt(Se(e)?e.call(this,this):e,Se(t)?t.call(this,this):t)}:t:e}function ig(e,t){return Ci(_o(e),_o(t))}function _o(e){if(Ee(e)){const t={};for(let n=0;n0)&&!(o&16)){if(o&8){const m=e.vnode.dynamicProps;for(let w=0;w{c=!0;const[C,O]=Zf(w,t,!0);Nt(o,C),O&&u.push(...O)};!n&&t.mixins.length&&t.mixins.forEach(m),e.extends&&m(e.extends),e.mixins&&e.mixins.forEach(m)}if(!s&&!c)return at(e)&&r.set(e,Vi),Vi;if(Ee(s))for(let m=0;m-1,O[1]=D<0||N-1||Me(O,"default"))&&u.push(w)}}}const p=[o,u];return at(e)&&r.set(e,p),p}function Hu(e){return e[0]!=="$"}function Fu(e){const t=e&&e.toString().match(/^\s*function (\w+)/);return t?t[1]:e===null?"null":""}function qu(e,t){return Fu(e)===Fu(t)}function Bu(e,t){return Ee(t)?t.findIndex(n=>qu(n,e)):Se(t)&&qu(t,e)?0:-1}const ed=e=>e[0]==="_"||e==="$stable",dl=e=>Ee(e)?e.map(xn):[xn(e)],og=(e,t,n)=>{if(t._n)return t;const r=Pm((...i)=>dl(t(...i)),n);return r._c=!1,r},td=(e,t,n)=>{const r=e._ctx;for(const i in e){if(ed(i))continue;const s=e[i];if(Se(s))t[i]=og(i,s,r);else if(s!=null){const o=dl(s);t[i]=()=>o}}},nd=(e,t)=>{const n=dl(t);e.slots.default=()=>n},lg=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=Ue(t),qs(t,"_",n)):td(t,e.slots={})}else e.slots={},t&&nd(e,t);qs(e.slots,da,1)},ug=(e,t,n)=>{const{vnode:r,slots:i}=e;let s=!0,o=tt;if(r.shapeFlag&32){const u=t._;u?n&&u===1?s=!1:(Nt(i,t),!n&&u===1&&delete i._):(s=!t.$stable,td(t,i)),o=t}else t&&(nd(e,t),o={default:1});if(s)for(const u in i)!ed(u)&&!(u in o)&&delete i[u]};function id(){return{app:null,config:{isNativeTag:qp,performance:!1,globalProperties:{},optionMergeStrategies:{},errorHandler:void 0,warnHandler:void 0,compilerOptions:{}},mixins:[],components:{},directives:{},provides:Object.create(null),optionsCache:new WeakMap,propsCache:new WeakMap,emitsCache:new WeakMap}}let cg=0;function fg(e,t){return function(r,i=null){Se(r)||(r=Object.assign({},r)),i!=null&&!at(i)&&(i=null);const s=id(),o=new Set;let u=!1;const c=s.app={_uid:cg++,_component:r,_props:i,_container:null,_context:s,_instance:null,version:Rg,get config(){return s.config},set config(p){},use(p,...m){return o.has(p)||(p&&Se(p.install)?(o.add(p),p.install(c,...m)):Se(p)&&(o.add(p),p(c,...m))),c},mixin(p){return s.mixins.includes(p)||s.mixins.push(p),c},component(p,m){return m?(s.components[p]=m,c):s.components[p]},directive(p,m){return m?(s.directives[p]=m,c):s.directives[p]},mount(p,m,w){if(!u){const C=Ae(r,i);return C.appContext=s,m&&t?t(C,p):e(C,p,w),u=!0,c._container=p,p.__vue_app__=c,ha(C.component)||C.component.proxy}},unmount(){u&&(e(null,c._container),delete c._container.__vue_app__)},provide(p,m){return s.provides[p]=m,c}};return c}}function ko(e,t,n,r,i=!1){if(Ee(e)){e.forEach((C,O)=>ko(C,t&&(Ee(t)?t[O]:t),n,r,i));return}if(Ps(r)&&!i)return;const s=r.shapeFlag&4?ha(r.component)||r.component.proxy:r.el,o=i?null:s,{i:u,r:c}=e,p=t&&t.r,m=u.refs===tt?u.refs={}:u.refs,w=u.setupState;if(p!=null&&p!==c&&(xt(p)?(m[p]=null,Me(w,p)&&(w[p]=null)):Lt(p)&&(p.value=null)),Se(c))si(c,u,12,[o,m]);else{const C=xt(c),O=Lt(c);if(C||O){const N=()=>{if(e.f){const D=C?Me(w,c)?w[c]:m[c]:c.value;i?Ee(D)&&Jo(D,s):Ee(D)?D.includes(s)||D.push(s):C?(m[c]=[s],Me(w,c)&&(w[c]=m[c])):(c.value=[s],e.k&&(m[e.k]=c.value))}else C?(m[c]=o,Me(w,c)&&(w[c]=o)):O&&(c.value=o,e.k&&(m[e.k]=o))};o?(N.id=-1,Ut(N,n)):N()}}}const Ut=Hm;function dg(e){return hg(e)}function hg(e,t){const n=Yp();n.__VUE__=!0;const{insert:r,remove:i,patchProp:s,createElement:o,createText:u,createComment:c,setText:p,setElementText:m,parentNode:w,nextSibling:C,setScopeId:O=gn,insertStaticContent:N}=e,D=(k,E,H,B=null,L=null,Q=null,se=!1,J=null,ne=!!E.dynamicChildren)=>{if(k===E)return;k&&!br(k,E)&&(B=ie(k),Ot(k,L,Q,!0),k=null),E.patchFlag===-2&&(ne=!1,E.dynamicChildren=null);const{type:G,ref:me,shapeFlag:ue}=E;switch(G){case fa:ee(k,E,H,B);break;case Ri:R(k,E,H,B);break;case Ga:k==null&&j(E,H,B,se);break;case Xt:Z(k,E,H,B,L,Q,se,J,ne);break;default:ue&1?ke(k,E,H,B,L,Q,se,J,ne):ue&6?qe(k,E,H,B,L,Q,se,J,ne):(ue&64||ue&128)&&G.process(k,E,H,B,L,Q,se,J,ne,$e)}me!=null&&L&&ko(me,k&&k.ref,Q,E||k,!E)},ee=(k,E,H,B)=>{if(k==null)r(E.el=u(E.children),H,B);else{const L=E.el=k.el;E.children!==k.children&&p(L,E.children)}},R=(k,E,H,B)=>{k==null?r(E.el=c(E.children||""),H,B):E.el=k.el},j=(k,E,H,B)=>{[k.el,k.anchor]=N(k.children,E,H,B,k.el,k.anchor)},K=({el:k,anchor:E},H,B)=>{let L;for(;k&&k!==E;)L=C(k),r(k,H,B),k=L;r(E,H,B)},ae=({el:k,anchor:E})=>{let H;for(;k&&k!==E;)H=C(k),i(k),k=H;i(E)},ke=(k,E,H,B,L,Q,se,J,ne)=>{se=se||E.type==="svg",k==null?h(E,H,B,L,Q,se,J,ne):Ze(k,E,L,Q,se,J,ne)},h=(k,E,H,B,L,Q,se,J)=>{let ne,G;const{type:me,props:ue,shapeFlag:ce,transition:ve,dirs:Te}=k;if(ne=k.el=o(k.type,Q,ue&&ue.is,ue),ce&8?m(ne,k.children):ce&16&&Xe(k.children,ne,null,B,L,Q&&me!=="foreignObject",se,J),Te&&wi(k,null,B,"created"),ue){for(const Ie in ue)Ie!=="value"&&!Rs(Ie)&&s(ne,Ie,null,ue[Ie],Q,k.children,B,L,oe);"value"in ue&&s(ne,"value",null,ue.value),(G=ue.onVnodeBeforeMount)&&kn(G,B,k)}He(ne,k,k.scopeId,se,B),Te&&wi(k,null,B,"beforeMount");const Ye=(!L||L&&!L.pendingBranch)&&ve&&!ve.persisted;Ye&&ve.beforeEnter(ne),r(ne,E,H),((G=ue&&ue.onVnodeMounted)||Ye||Te)&&Ut(()=>{G&&kn(G,B,k),Ye&&ve.enter(ne),Te&&wi(k,null,B,"mounted")},L)},He=(k,E,H,B,L)=>{if(H&&O(k,H),B)for(let Q=0;Q{for(let G=ne;G{const J=E.el=k.el;let{patchFlag:ne,dynamicChildren:G,dirs:me}=E;ne|=k.patchFlag&16;const ue=k.props||tt,ce=E.props||tt;let ve;H&&ki(H,!1),(ve=ce.onVnodeBeforeUpdate)&&kn(ve,H,E,k),me&&wi(E,k,H,"beforeUpdate"),H&&ki(H,!0);const Te=L&&E.type!=="foreignObject";if(G?Tt(k.dynamicChildren,G,J,H,B,Te,Q):se||Be(k,E,J,null,H,B,Te,Q,!1),ne>0){if(ne&16)Mt(J,E,ue,ce,H,B,L);else if(ne&2&&ue.class!==ce.class&&s(J,"class",null,ce.class,L),ne&4&&s(J,"style",ue.style,ce.style,L),ne&8){const Ye=E.dynamicProps;for(let Ie=0;Ie{ve&&kn(ve,H,E,k),me&&wi(E,k,H,"updated")},B)},Tt=(k,E,H,B,L,Q,se)=>{for(let J=0;J{if(H!==B){if(H!==tt)for(const J in H)!Rs(J)&&!(J in B)&&s(k,J,H[J],null,se,E.children,L,Q,oe);for(const J in B){if(Rs(J))continue;const ne=B[J],G=H[J];ne!==G&&J!=="value"&&s(k,J,G,ne,se,E.children,L,Q,oe)}"value"in B&&s(k,"value",H.value,B.value)}},Z=(k,E,H,B,L,Q,se,J,ne)=>{const G=E.el=k?k.el:u(""),me=E.anchor=k?k.anchor:u("");let{patchFlag:ue,dynamicChildren:ce,slotScopeIds:ve}=E;ve&&(J=J?J.concat(ve):ve),k==null?(r(G,H,B),r(me,H,B),Xe(E.children,H,me,L,Q,se,J,ne)):ue>0&&ue&64&&ce&&k.dynamicChildren?(Tt(k.dynamicChildren,ce,H,L,Q,se,J),(E.key!=null||L&&E===L.subTree)&&rd(k,E,!0)):Be(k,E,H,me,L,Q,se,J,ne)},qe=(k,E,H,B,L,Q,se,J,ne)=>{E.slotScopeIds=J,k==null?E.shapeFlag&512?L.ctx.activate(E,H,B,se,ne):dt(E,H,B,L,Q,se,ne):Dt(k,E,ne)},dt=(k,E,H,B,L,Q,se)=>{const J=k.component=kg(k,B,L);if(Vf(k)&&(J.ctx.renderer=$e),Cg(J),J.asyncDep){if(L&&L.registerDep(J,ht),!k.el){const ne=J.subTree=Ae(Ri);R(null,ne,E,H)}return}ht(J,k,E,H,L,Q,se)},Dt=(k,E,H)=>{const B=E.component=k.component;if(Nm(k,E,H))if(B.asyncDep&&!B.asyncResolved){Ve(B,E,H);return}else B.next=E,Sm(B.update),B.update();else E.el=k.el,B.vnode=E},ht=(k,E,H,B,L,Q,se)=>{const J=()=>{if(k.isMounted){let{next:me,bu:ue,u:ce,parent:ve,vnode:Te}=k,Ye=me,Ie;ki(k,!1),me?(me.el=Te.el,Ve(k,me,se)):me=Te,ue&&Ya(ue),(Ie=me.props&&me.props.onVnodeBeforeUpdate)&&kn(Ie,ve,me,Te),ki(k,!0);const ut=Ka(k),Ht=k.subTree;k.subTree=ut,D(Ht,ut,w(Ht.el),ie(Ht),k,L,Q),me.el=ut.el,Ye===null&&Mm(k,ut.el),ce&&Ut(ce,L),(Ie=me.props&&me.props.onVnodeUpdated)&&Ut(()=>kn(Ie,ve,me,Te),L)}else{let me;const{el:ue,props:ce}=E,{bm:ve,m:Te,parent:Ye}=k,Ie=Ps(E);if(ki(k,!1),ve&&Ya(ve),!Ie&&(me=ce&&ce.onVnodeBeforeMount)&&kn(me,Ye,E),ki(k,!0),ue&&Oe){const ut=()=>{k.subTree=Ka(k),Oe(ue,k.subTree,k,L,null)};Ie?E.type.__asyncLoader().then(()=>!k.isUnmounted&&ut()):ut()}else{const ut=k.subTree=Ka(k);D(null,ut,H,B,k,L,Q),E.el=ut.el}if(Te&&Ut(Te,L),!Ie&&(me=ce&&ce.onVnodeMounted)){const ut=E;Ut(()=>kn(me,Ye,ut),L)}(E.shapeFlag&256||Ye&&Ps(Ye.vnode)&&Ye.vnode.shapeFlag&256)&&k.a&&Ut(k.a,L),k.isMounted=!0,E=H=B=null}},ne=k.effect=new nl(J,()=>cl(G),k.scope),G=k.update=()=>ne.run();G.id=k.uid,ki(k,!0),G()},Ve=(k,E,H)=>{E.component=k;const B=k.vnode.props;k.vnode=E,k.next=null,ag(k,E.props,B,H),ug(k,E.children,H),sr(),Lu(),ar()},Be=(k,E,H,B,L,Q,se,J,ne=!1)=>{const G=k&&k.children,me=k?k.shapeFlag:0,ue=E.children,{patchFlag:ce,shapeFlag:ve}=E;if(ce>0){if(ce&128){wn(G,ue,H,B,L,Q,se,J,ne);return}else if(ce&256){Wt(G,ue,H,B,L,Q,se,J,ne);return}}ve&8?(me&16&&oe(G,L,Q),ue!==G&&m(H,ue)):me&16?ve&16?wn(G,ue,H,B,L,Q,se,J,ne):oe(G,L,Q,!0):(me&8&&m(H,""),ve&16&&Xe(ue,H,B,L,Q,se,J,ne))},Wt=(k,E,H,B,L,Q,se,J,ne)=>{k=k||Vi,E=E||Vi;const G=k.length,me=E.length,ue=Math.min(G,me);let ce;for(ce=0;ceme?oe(k,L,Q,!0,!1,ue):Xe(E,H,B,L,Q,se,J,ne,ue)},wn=(k,E,H,B,L,Q,se,J,ne)=>{let G=0;const me=E.length;let ue=k.length-1,ce=me-1;for(;G<=ue&&G<=ce;){const ve=k[G],Te=E[G]=ne?Xn(E[G]):xn(E[G]);if(br(ve,Te))D(ve,Te,H,null,L,Q,se,J,ne);else break;G++}for(;G<=ue&&G<=ce;){const ve=k[ue],Te=E[ce]=ne?Xn(E[ce]):xn(E[ce]);if(br(ve,Te))D(ve,Te,H,null,L,Q,se,J,ne);else break;ue--,ce--}if(G>ue){if(G<=ce){const ve=ce+1,Te=vece)for(;G<=ue;)Ot(k[G],L,Q,!0),G++;else{const ve=G,Te=G,Ye=new Map;for(G=Te;G<=ce;G++){const Rt=E[G]=ne?Xn(E[G]):xn(E[G]);Rt.key!=null&&Ye.set(Rt.key,G)}let Ie,ut=0;const Ht=ce-Te+1;let Wn=!1,Ln=0;const on=new Array(Ht);for(G=0;G=Ht){Ot(Rt,L,Q,!0);continue}let ct;if(Rt.key!=null)ct=Ye.get(Rt.key);else for(Ie=Te;Ie<=ce;Ie++)if(on[Ie-Te]===0&&br(Rt,E[Ie])){ct=Ie;break}ct===void 0?Ot(Rt,L,Q,!0):(on[ct-Te]=G+1,ct>=Ln?Ln=ct:Wn=!0,D(Rt,E[ct],H,null,L,Q,se,J,ne),ut++)}const cr=Wn?pg(on):Vi;for(Ie=cr.length-1,G=Ht-1;G>=0;G--){const Rt=Te+G,ct=E[Rt],At=Rt+1{const{el:Q,type:se,transition:J,children:ne,shapeFlag:G}=k;if(G&6){pt(k.component.subTree,E,H,B);return}if(G&128){k.suspense.move(E,H,B);return}if(G&64){se.move(k,E,H,$e);return}if(se===Xt){r(Q,E,H);for(let ue=0;ueJ.enter(Q),L);else{const{leave:ue,delayLeave:ce,afterLeave:ve}=J,Te=()=>r(Q,E,H),Ye=()=>{ue(Q,()=>{Te(),ve&&ve()})};ce?ce(Q,Te,Ye):Ye()}else r(Q,E,H)},Ot=(k,E,H,B=!1,L=!1)=>{const{type:Q,props:se,ref:J,children:ne,dynamicChildren:G,shapeFlag:me,patchFlag:ue,dirs:ce}=k;if(J!=null&&ko(J,null,H,k,!0),me&256){E.ctx.deactivate(k);return}const ve=me&1&&ce,Te=!Ps(k);let Ye;if(Te&&(Ye=se&&se.onVnodeBeforeUnmount)&&kn(Ye,E,k),me&6)W(k.component,H,B);else{if(me&128){k.suspense.unmount(H,B);return}ve&&wi(k,null,E,"beforeUnmount"),me&64?k.type.remove(k,E,H,L,$e,B):G&&(Q!==Xt||ue>0&&ue&64)?oe(G,E,H,!1,!0):(Q===Xt&&ue&384||!L&&me&16)&&oe(ne,E,H),B&&Vt(k)}(Te&&(Ye=se&&se.onVnodeUnmounted)||ve)&&Ut(()=>{Ye&&kn(Ye,E,k),ve&&wi(k,null,E,"unmounted")},H)},Vt=k=>{const{type:E,el:H,anchor:B,transition:L}=k;if(E===Xt){Pn(H,B);return}if(E===Ga){ae(k);return}const Q=()=>{i(H),L&&!L.persisted&&L.afterLeave&&L.afterLeave()};if(k.shapeFlag&1&&L&&!L.persisted){const{leave:se,delayLeave:J}=L,ne=()=>se(H,Q);J?J(k.el,Q,ne):ne()}else Q()},Pn=(k,E)=>{let H;for(;k!==E;)H=C(k),i(k),k=H;i(E)},W=(k,E,H)=>{const{bum:B,scope:L,update:Q,subTree:se,um:J}=k;B&&Ya(B),L.stop(),Q&&(Q.active=!1,Ot(se,k,E,H)),J&&Ut(J,E),Ut(()=>{k.isUnmounted=!0},E),E&&E.pendingBranch&&!E.isUnmounted&&k.asyncDep&&!k.asyncResolved&&k.suspenseId===E.pendingId&&(E.deps--,E.deps===0&&E.resolve())},oe=(k,E,H,B=!1,L=!1,Q=0)=>{for(let se=Q;sek.shapeFlag&6?ie(k.component.subTree):k.shapeFlag&128?k.suspense.next():C(k.anchor||k.el),de=(k,E,H)=>{k==null?E._vnode&&Ot(E._vnode,null,null,!0):D(E._vnode||null,k,E,null,null,null,H),Lu(),qf(),E._vnode=k},$e={p:D,um:Ot,m:pt,r:Vt,mt:dt,mc:Xe,pc:Be,pbc:Tt,n:ie,o:e};let et,Oe;return t&&([et,Oe]=t($e)),{render:de,hydrate:et,createApp:fg(de,et)}}function ki({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function rd(e,t,n=!1){const r=e.children,i=t.children;if(Ee(r)&&Ee(i))for(let s=0;s>1,e[n[u]]0&&(t[r]=n[s-1]),n[s]=r)}}for(s=n.length,o=n[s-1];s-- >0;)n[s]=o,o=t[o];return n}const mg=e=>e.__isTeleport,Xt=Symbol(void 0),fa=Symbol(void 0),Ri=Symbol(void 0),Ga=Symbol(void 0),Or=[];let pn=null;function Re(e=!1){Or.push(pn=e?null:[])}function gg(){Or.pop(),pn=Or[Or.length-1]||null}let qr=1;function ju(e){qr+=e}function sd(e){return e.dynamicChildren=qr>0?pn||Vi:null,gg(),qr>0&&pn&&pn.push(e),e}function De(e,t,n,r,i,s){return sd(X(e,t,n,r,i,s,!0))}function zi(e,t,n,r,i){return sd(Ae(e,t,n,r,i,!0))}function Co(e){return e?e.__v_isVNode===!0:!1}function br(e,t){return e.type===t.type&&e.key===t.key}const da="__vInternal",ad=({key:e})=>e!=null?e:null,Ls=({ref:e,ref_key:t,ref_for:n})=>e!=null?xt(e)||Lt(e)||Se(e)?{i:rn,r:e,k:t,f:!!n}:e:null;function X(e,t=null,n=null,r=0,i=null,s=e===Xt?0:1,o=!1,u=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&ad(t),ref:t&&Ls(t),scopeId:la,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:r,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:rn};return u?(hl(c,n),s&128&&e.normalize(c)):n&&(c.shapeFlag|=xt(n)?8:16),qr>0&&!o&&pn&&(c.patchFlag>0||s&6)&&c.patchFlag!==32&&pn.push(c),c}const Ae=vg;function vg(e,t=null,n=null,r=0,i=null,s=!1){if((!e||e===Xm)&&(e=Ri),Co(e)){const u=Zi(e,t,!0);return n&&hl(u,n),qr>0&&!s&&pn&&(u.shapeFlag&6?pn[pn.indexOf(e)]=u:pn.push(u)),u.patchFlag|=-2,u}if(Eg(e)&&(e=e.__vccOpts),t){t=yg(t);let{class:u,style:c}=t;u&&!xt(u)&&(t.class=Xi(u)),at(c)&&(Rf(c)&&!Ee(c)&&(c=Nt({},c)),t.style=Go(c))}const o=xt(e)?1:Dm(e)?128:mg(e)?64:at(e)?4:Se(e)?2:0;return X(e,t,n,r,i,o,s,!0)}function yg(e){return e?Rf(e)||da in e?Nt({},e):e:null}function Zi(e,t,n=!1){const{props:r,ref:i,patchFlag:s,children:o}=e,u=t?bg(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&ad(u),ref:t&&t.ref?n&&i?Ee(i)?i.concat(Ls(t)):[i,Ls(t)]:Ls(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Xt?s===-1?16:s|16:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Zi(e.ssContent),ssFallback:e.ssFallback&&Zi(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx}}function sn(e=" ",t=0){return Ae(fa,null,e,t)}function Zn(e="",t=!1){return t?(Re(),zi(Ri,null,e)):Ae(Ri,null,e)}function xn(e){return e==null||typeof e=="boolean"?Ae(Ri):Ee(e)?Ae(Xt,null,e.slice()):typeof e=="object"?Xn(e):Ae(fa,null,String(e))}function Xn(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Zi(e)}function hl(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(Ee(t))n=16;else if(typeof t=="object")if(r&65){const i=t.default;i&&(i._c&&(i._d=!1),hl(e,i()),i._c&&(i._d=!0));return}else{n=32;const i=t._;!i&&!(da in t)?t._ctx=rn:i===3&&rn&&(rn.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else Se(t)?(t={default:t,_ctx:rn},n=32):(t=String(t),r&64?(n=16,t=[sn(t)]):n=8);e.children=t,e.shapeFlag|=n}function bg(...e){const t={};for(let n=0;n{Et=e,e.scope.on()},Oi=()=>{Et&&Et.scope.off(),Et=null};function od(e){return e.vnode.shapeFlag&4}let Br=!1;function Cg(e,t=!1){Br=t;const{props:n,children:r}=e.vnode,i=od(e);sg(e,n,i,t),lg(e,r);const s=i?xg(e,t):void 0;return Br=!1,s}function xg(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=zf(new Proxy(e.ctx,Zm));const{setup:r}=n;if(r){const i=e.setupContext=r.length>1?Tg(e):null;er(e),sr();const s=si(r,e,0,[e.props,i]);if(ar(),Oi(),gf(s)){if(s.then(Oi,Oi),t)return s.then(o=>{Uu(e,o,t)}).catch(o=>{aa(o,e,0)});e.asyncDep=s}else Uu(e,s,t)}else ld(e,t)}function Uu(e,t,n){Se(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:at(t)&&(e.setupState=Nf(t)),ld(e,n)}let Wu;function ld(e,t,n){const r=e.type;if(!e.render){if(!t&&Wu&&!r.render){const i=r.template||fl(e).template;if(i){const{isCustomElement:s,compilerOptions:o}=e.appContext.config,{delimiters:u,compilerOptions:c}=r,p=Nt(Nt({isCustomElement:s,delimiters:u},o),c);r.render=Wu(i,p)}}e.render=r.render||gn}er(e),sr(),eg(e),ar(),Oi()}function $g(e){return new Proxy(e.attrs,{get(t,n){return Zt(e,"get","$attrs"),t[n]}})}function Tg(e){const t=r=>{e.exposed=r||{}};let n;return{get attrs(){return n||(n=$g(e))},slots:e.slots,emit:e.emit,expose:t}}function ha(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Nf(zf(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Sr)return Sr[n](e)},has(t,n){return n in t||n in Sr}}))}function Ag(e,t=!0){return Se(e)?e.displayName||e.name:e.name||t&&e.__name}function Eg(e){return Se(e)&&"__vccOpts"in e}const st=(e,t)=>Tm(e,t,Br);function pa(e,t,n){const r=arguments.length;return r===2?at(t)&&!Ee(t)?Co(t)?Ae(e,null,[t]):Ae(e,t):Ae(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&Co(n)&&(n=[n]),Ae(e,t,n))}const Sg=Symbol(""),Og=()=>yn(Sg),Rg="3.2.45",zg="http://www.w3.org/2000/svg",$i=typeof document<"u"?document:null,Vu=$i&&$i.createElement("template"),Pg={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const i=t?$i.createElementNS(zg,e):$i.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&i.setAttribute("multiple",r.multiple),i},createText:e=>$i.createTextNode(e),createComment:e=>$i.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>$i.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,i,s){const o=n?n.previousSibling:t.lastChild;if(i&&(i===s||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),n),!(i===s||!(i=i.nextSibling)););else{Vu.innerHTML=r?`${e}`:e;const u=Vu.content;if(r){const c=u.firstChild;for(;c.firstChild;)u.appendChild(c.firstChild);u.removeChild(c)}t.insertBefore(u,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function Lg(e,t,n){const r=e._vtc;r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function Ig(e,t,n){const r=e.style,i=xt(n);if(n&&!i){for(const s in n)xo(r,s,n[s]);if(t&&!xt(t))for(const s in t)n[s]==null&&xo(r,s,"")}else{const s=r.display;i?t!==n&&(r.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(r.display=s)}}const Yu=/\s*!important$/;function xo(e,t,n){if(Ee(n))n.forEach(r=>xo(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Ng(e,t);Yu.test(n)?e.setProperty(Ni(r),n.replace(Yu,""),"important"):e[r]=n}}const Ku=["Webkit","Moz","ms"],Xa={};function Ng(e,t){const n=Xa[t];if(n)return n;let r=On(t);if(r!=="filter"&&r in e)return Xa[t]=r;r=ra(r);for(let i=0;iJa||(jg.then(()=>Ja=0),Ja=Date.now());function Wg(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;vn(Vg(r,n.value),t,5,[r])};return n.value=e,n.attached=Ug(),n}function Vg(e,t){if(Ee(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>i=>!i._stopped&&r&&r(i))}else return t}const Xu=/^on[a-z]/,Yg=(e,t,n,r,i=!1,s,o,u,c)=>{t==="class"?Lg(e,r,i):t==="style"?Ig(e,n,r):ta(t)?Xo(t)||qg(e,t,n,r,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Kg(e,t,r,i))?Dg(e,t,r,s,o,u,c):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Mg(e,t,r,i))};function Kg(e,t,n,r){return r?!!(t==="innerHTML"||t==="textContent"||t in e&&Xu.test(t)&&Se(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||Xu.test(t)&&xt(n)?!1:t in e}const Qg={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},ma=(e,t)=>n=>{if(!("key"in n))return;const r=Ni(n.key);if(t.some(i=>i===r||Qg[i]===r))return e(n)},jr={beforeMount(e,{value:t},{transition:n}){e._vod=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):_r(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),_r(e,!0),r.enter(e)):r.leave(e,()=>{_r(e,!1)}):_r(e,t))},beforeUnmount(e,{value:t}){_r(e,t)}};function _r(e,t){e.style.display=t?e._vod:"none"}const Gg=Nt({patchProp:Yg},Pg);let Ju;function Xg(){return Ju||(Ju=dg(Gg))}const Jg=(...e)=>{const t=Xg().createApp(...e),{mount:n}=t;return t.mount=r=>{const i=Zg(r);if(!i)return;const s=t._component;!Se(s)&&!s.render&&!s.template&&(s.template=i.innerHTML),i.innerHTML="";const o=n(i,!1,i instanceof SVGElement);return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),o},t};function Zg(e){return xt(e)?document.querySelector(e):e}/*! + * vue-router v4.1.6 + * (c) 2022 Eduardo San Martin Morote + * @license MIT + */const ji=typeof window<"u";function ev(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const We=Object.assign;function Za(e,t){const n={};for(const r in t){const i=t[r];n[r]=_n(i)?i.map(e):e(i)}return n}const Rr=()=>{},_n=Array.isArray,tv=/\/$/,nv=e=>e.replace(tv,"");function eo(e,t,n="/"){let r,i={},s="",o="";const u=t.indexOf("#");let c=t.indexOf("?");return u=0&&(c=-1),c>-1&&(r=t.slice(0,c),s=t.slice(c+1,u>-1?u:t.length),i=e(s)),u>-1&&(r=r||t.slice(0,u),o=t.slice(u,t.length)),r=av(r!=null?r:t,n),{fullPath:r+(s&&"?")+s+o,path:r,query:i,hash:o}}function iv(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Zu(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function rv(e,t,n){const r=t.matched.length-1,i=n.matched.length-1;return r>-1&&r===i&&tr(t.matched[r],n.matched[i])&&ud(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function tr(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function ud(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!sv(e[n],t[n]))return!1;return!0}function sv(e,t){return _n(e)?ec(e,t):_n(t)?ec(t,e):e===t}function ec(e,t){return _n(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function av(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/");let i=n.length-1,s,o;for(s=0;s1&&i--;else break;return n.slice(0,i).join("/")+"/"+r.slice(s-(s===r.length?1:0)).join("/")}var Ur;(function(e){e.pop="pop",e.push="push"})(Ur||(Ur={}));var zr;(function(e){e.back="back",e.forward="forward",e.unknown=""})(zr||(zr={}));function ov(e){if(!e)if(ji){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),nv(e)}const lv=/^[^#]+#/;function uv(e,t){return e.replace(lv,"#")+t}function cv(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const ga=()=>({left:window.pageXOffset,top:window.pageYOffset});function fv(e){let t;if("el"in e){const n=e.el,r=typeof n=="string"&&n.startsWith("#"),i=typeof n=="string"?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!i)return;t=cv(i,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.pageXOffset,t.top!=null?t.top:window.pageYOffset)}function tc(e,t){return(history.state?history.state.position-t:-1)+e}const $o=new Map;function dv(e,t){$o.set(e,t)}function hv(e){const t=$o.get(e);return $o.delete(e),t}let pv=()=>location.protocol+"//"+location.host;function cd(e,t){const{pathname:n,search:r,hash:i}=t,s=e.indexOf("#");if(s>-1){let u=i.includes(e.slice(s))?e.slice(s).length:1,c=i.slice(u);return c[0]!=="/"&&(c="/"+c),Zu(c,"")}return Zu(n,e)+r+i}function mv(e,t,n,r){let i=[],s=[],o=null;const u=({state:C})=>{const O=cd(e,location),N=n.value,D=t.value;let ee=0;if(C){if(n.value=O,t.value=C,o&&o===N){o=null;return}ee=D?C.position-D.position:0}else r(O);i.forEach(R=>{R(n.value,N,{delta:ee,type:Ur.pop,direction:ee?ee>0?zr.forward:zr.back:zr.unknown})})};function c(){o=n.value}function p(C){i.push(C);const O=()=>{const N=i.indexOf(C);N>-1&&i.splice(N,1)};return s.push(O),O}function m(){const{history:C}=window;!C.state||C.replaceState(We({},C.state,{scroll:ga()}),"")}function w(){for(const C of s)C();s=[],window.removeEventListener("popstate",u),window.removeEventListener("beforeunload",m)}return window.addEventListener("popstate",u),window.addEventListener("beforeunload",m),{pauseListeners:c,listen:p,destroy:w}}function nc(e,t,n,r=!1,i=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:i?ga():null}}function gv(e){const{history:t,location:n}=window,r={value:cd(e,n)},i={value:t.state};i.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function s(c,p,m){const w=e.indexOf("#"),C=w>-1?(n.host&&document.querySelector("base")?e:e.slice(w))+c:pv()+e+c;try{t[m?"replaceState":"pushState"](p,"",C),i.value=p}catch(O){console.error(O),n[m?"replace":"assign"](C)}}function o(c,p){const m=We({},t.state,nc(i.value.back,c,i.value.forward,!0),p,{position:i.value.position});s(c,m,!0),r.value=c}function u(c,p){const m=We({},i.value,t.state,{forward:c,scroll:ga()});s(m.current,m,!0);const w=We({},nc(r.value,c,null),{position:m.position+1},p);s(c,w,!1),r.value=c}return{location:r,state:i,push:u,replace:o}}function vv(e){e=ov(e);const t=gv(e),n=mv(e,t.state,t.location,t.replace);function r(s,o=!0){o||n.pauseListeners(),history.go(s)}const i=We({location:"",base:e,go:r,createHref:uv.bind(null,e)},t,n);return Object.defineProperty(i,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(i,"state",{enumerable:!0,get:()=>t.state.value}),i}function yv(e){return typeof e=="string"||e&&typeof e=="object"}function fd(e){return typeof e=="string"||typeof e=="symbol"}const Qn={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},dd=Symbol("");var ic;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(ic||(ic={}));function nr(e,t){return We(new Error,{type:e,[dd]:!0},t)}function Mn(e,t){return e instanceof Error&&dd in e&&(t==null||!!(e.type&t))}const rc="[^/]+?",bv={sensitive:!1,strict:!1,start:!0,end:!0},_v=/[.+*?^${}()[\]/\\]/g;function wv(e,t){const n=We({},bv,t),r=[];let i=n.start?"^":"";const s=[];for(const p of e){const m=p.length?[]:[90];n.strict&&!p.length&&(i+="/");for(let w=0;wt.length?t.length===1&&t[0]===40+40?1:-1:0}function Cv(e,t){let n=0;const r=e.score,i=t.score;for(;n0&&t[t.length-1]<0}const xv={type:0,value:""},$v=/[a-zA-Z0-9_]/;function Tv(e){if(!e)return[[]];if(e==="/")return[[xv]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(O){throw new Error(`ERR (${n})/"${p}": ${O}`)}let n=0,r=n;const i=[];let s;function o(){s&&i.push(s),s=[]}let u=0,c,p="",m="";function w(){!p||(n===0?s.push({type:0,value:p}):n===1||n===2||n===3?(s.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${p}) must be alone in its segment. eg: '/:ids+.`),s.push({type:1,value:p,regexp:m,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),p="")}function C(){p+=c}for(;u{o(j)}:Rr}function o(m){if(fd(m)){const w=r.get(m);w&&(r.delete(m),n.splice(n.indexOf(w),1),w.children.forEach(o),w.alias.forEach(o))}else{const w=n.indexOf(m);w>-1&&(n.splice(w,1),m.record.name&&r.delete(m.record.name),m.children.forEach(o),m.alias.forEach(o))}}function u(){return n}function c(m){let w=0;for(;w=0&&(m.record.path!==n[w].record.path||!hd(m,n[w]));)w++;n.splice(w,0,m),m.record.name&&!oc(m)&&r.set(m.record.name,m)}function p(m,w){let C,O={},N,D;if("name"in m&&m.name){if(C=r.get(m.name),!C)throw nr(1,{location:m});D=C.record.name,O=We(ac(w.params,C.keys.filter(j=>!j.optional).map(j=>j.name)),m.params&&ac(m.params,C.keys.map(j=>j.name))),N=C.stringify(O)}else if("path"in m)N=m.path,C=n.find(j=>j.re.test(N)),C&&(O=C.parse(N),D=C.record.name);else{if(C=w.name?r.get(w.name):n.find(j=>j.re.test(w.path)),!C)throw nr(1,{location:m,currentLocation:w});D=C.record.name,O=We({},w.params,m.params),N=C.stringify(O)}const ee=[];let R=C;for(;R;)ee.unshift(R.record),R=R.parent;return{name:D,path:N,params:O,matched:ee,meta:Rv(ee)}}return e.forEach(m=>s(m)),{addRoute:s,resolve:p,removeRoute:o,getRoutes:u,getRecordMatcher:i}}function ac(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function Sv(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:Ov(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function Ov(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]=typeof n=="boolean"?n:n[r];return t}function oc(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Rv(e){return e.reduce((t,n)=>We(t,n.meta),{})}function lc(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function hd(e,t){return t.children.some(n=>n===e||hd(e,n))}const pd=/#/g,zv=/&/g,Pv=/\//g,Lv=/=/g,Iv=/\?/g,md=/\+/g,Nv=/%5B/g,Mv=/%5D/g,gd=/%5E/g,Dv=/%60/g,vd=/%7B/g,Hv=/%7C/g,yd=/%7D/g,Fv=/%20/g;function pl(e){return encodeURI(""+e).replace(Hv,"|").replace(Nv,"[").replace(Mv,"]")}function qv(e){return pl(e).replace(vd,"{").replace(yd,"}").replace(gd,"^")}function To(e){return pl(e).replace(md,"%2B").replace(Fv,"+").replace(pd,"%23").replace(zv,"%26").replace(Dv,"`").replace(vd,"{").replace(yd,"}").replace(gd,"^")}function Bv(e){return To(e).replace(Lv,"%3D")}function jv(e){return pl(e).replace(pd,"%23").replace(Iv,"%3F")}function Uv(e){return e==null?"":jv(e).replace(Pv,"%2F")}function Vs(e){try{return decodeURIComponent(""+e)}catch{}return""+e}function Wv(e){const t={};if(e===""||e==="?")return t;const r=(e[0]==="?"?e.slice(1):e).split("&");for(let i=0;is&&To(s)):[r&&To(r)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+n,s!=null&&(t+="="+s))})}return t}function Vv(e){const t={};for(const n in e){const r=e[n];r!==void 0&&(t[n]=_n(r)?r.map(i=>i==null?null:""+i):r==null?r:""+r)}return t}const Yv=Symbol(""),cc=Symbol(""),va=Symbol(""),ml=Symbol(""),Ao=Symbol("");function wr(){let e=[];function t(r){return e.push(r),()=>{const i=e.indexOf(r);i>-1&&e.splice(i,1)}}function n(){e=[]}return{add:t,list:()=>e,reset:n}}function Jn(e,t,n,r,i){const s=r&&(r.enterCallbacks[i]=r.enterCallbacks[i]||[]);return()=>new Promise((o,u)=>{const c=w=>{w===!1?u(nr(4,{from:n,to:t})):w instanceof Error?u(w):yv(w)?u(nr(2,{from:t,to:w})):(s&&r.enterCallbacks[i]===s&&typeof w=="function"&&s.push(w),o())},p=e.call(r&&r.instances[i],t,n,c);let m=Promise.resolve(p);e.length<3&&(m=m.then(c)),m.catch(w=>u(w))})}function to(e,t,n,r){const i=[];for(const s of e)for(const o in s.components){let u=s.components[o];if(!(t!=="beforeRouteEnter"&&!s.instances[o]))if(Kv(u)){const p=(u.__vccOpts||u)[t];p&&i.push(Jn(p,n,r,s,o))}else{let c=u();i.push(()=>c.then(p=>{if(!p)return Promise.reject(new Error(`Couldn't resolve component "${o}" at "${s.path}"`));const m=ev(p)?p.default:p;s.components[o]=m;const C=(m.__vccOpts||m)[t];return C&&Jn(C,n,r,s,o)()}))}}return i}function Kv(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function fc(e){const t=yn(va),n=yn(ml),r=st(()=>t.resolve(ri(e.to))),i=st(()=>{const{matched:c}=r.value,{length:p}=c,m=c[p-1],w=n.matched;if(!m||!w.length)return-1;const C=w.findIndex(tr.bind(null,m));if(C>-1)return C;const O=dc(c[p-2]);return p>1&&dc(m)===O&&w[w.length-1].path!==O?w.findIndex(tr.bind(null,c[p-2])):C}),s=st(()=>i.value>-1&&Jv(n.params,r.value.params)),o=st(()=>i.value>-1&&i.value===n.matched.length-1&&ud(n.params,r.value.params));function u(c={}){return Xv(c)?t[ri(e.replace)?"replace":"push"](ri(e.to)).catch(Rr):Promise.resolve()}return{route:r,href:st(()=>r.value.href),isActive:s,isExactActive:o,navigate:u}}const Qv=Xr({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:fc,setup(e,{slots:t}){const n=or(fc(e)),{options:r}=yn(va),i=st(()=>({[hc(e.activeClass,r.linkActiveClass,"router-link-active")]:n.isActive,[hc(e.exactActiveClass,r.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const s=t.default&&t.default(n);return e.custom?s:pa("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},s)}}}),Gv=Qv;function Xv(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Jv(e,t){for(const n in t){const r=t[n],i=e[n];if(typeof r=="string"){if(r!==i)return!1}else if(!_n(i)||i.length!==r.length||r.some((s,o)=>s!==i[o]))return!1}return!0}function dc(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const hc=(e,t,n)=>e!=null?e:t!=null?t:n,Zv=Xr({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const r=yn(Ao),i=st(()=>e.route||r.value),s=yn(cc,0),o=st(()=>{let p=ri(s);const{matched:m}=i.value;let w;for(;(w=m[p])&&!w.components;)p++;return p}),u=st(()=>i.value.matched[o.value]);zs(cc,st(()=>o.value+1)),zs(Yv,u),zs(Ao,i);const c=ll();return Er(()=>[c.value,u.value,e.name],([p,m,w],[C,O,N])=>{m&&(m.instances[w]=p,O&&O!==m&&p&&p===C&&(m.leaveGuards.size||(m.leaveGuards=O.leaveGuards),m.updateGuards.size||(m.updateGuards=O.updateGuards))),p&&m&&(!O||!tr(m,O)||!C)&&(m.enterCallbacks[w]||[]).forEach(D=>D(p))},{flush:"post"}),()=>{const p=i.value,m=e.name,w=u.value,C=w&&w.components[m];if(!C)return pc(n.default,{Component:C,route:p});const O=w.props[m],N=O?O===!0?p.params:typeof O=="function"?O(p):O:null,ee=pa(C,We({},N,t,{onVnodeUnmounted:R=>{R.component.isUnmounted&&(w.instances[m]=null)},ref:c}));return pc(n.default,{Component:ee,route:p})||ee}}});function pc(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const ey=Zv;function ty(e){const t=Ev(e.routes,e),n=e.parseQuery||Wv,r=e.stringifyQuery||uc,i=e.history,s=wr(),o=wr(),u=wr(),c=km(Qn);let p=Qn;ji&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const m=Za.bind(null,W=>""+W),w=Za.bind(null,Uv),C=Za.bind(null,Vs);function O(W,oe){let ie,de;return fd(W)?(ie=t.getRecordMatcher(W),de=oe):de=W,t.addRoute(de,ie)}function N(W){const oe=t.getRecordMatcher(W);oe&&t.removeRoute(oe)}function D(){return t.getRoutes().map(W=>W.record)}function ee(W){return!!t.getRecordMatcher(W)}function R(W,oe){if(oe=We({},oe||c.value),typeof W=="string"){const k=eo(n,W,oe.path),E=t.resolve({path:k.path},oe),H=i.createHref(k.fullPath);return We(k,E,{params:C(E.params),hash:Vs(k.hash),redirectedFrom:void 0,href:H})}let ie;if("path"in W)ie=We({},W,{path:eo(n,W.path,oe.path).path});else{const k=We({},W.params);for(const E in k)k[E]==null&&delete k[E];ie=We({},W,{params:w(W.params)}),oe.params=w(oe.params)}const de=t.resolve(ie,oe),$e=W.hash||"";de.params=m(C(de.params));const et=iv(r,We({},W,{hash:qv($e),path:de.path})),Oe=i.createHref(et);return We({fullPath:et,hash:$e,query:r===uc?Vv(W.query):W.query||{}},de,{redirectedFrom:void 0,href:Oe})}function j(W){return typeof W=="string"?eo(n,W,c.value.path):We({},W)}function K(W,oe){if(p!==W)return nr(8,{from:oe,to:W})}function ae(W){return He(W)}function ke(W){return ae(We(j(W),{replace:!0}))}function h(W){const oe=W.matched[W.matched.length-1];if(oe&&oe.redirect){const{redirect:ie}=oe;let de=typeof ie=="function"?ie(W):ie;return typeof de=="string"&&(de=de.includes("?")||de.includes("#")?de=j(de):{path:de},de.params={}),We({query:W.query,hash:W.hash,params:"path"in de?{}:W.params},de)}}function He(W,oe){const ie=p=R(W),de=c.value,$e=W.state,et=W.force,Oe=W.replace===!0,k=h(ie);if(k)return He(We(j(k),{state:typeof k=="object"?We({},$e,k.state):$e,force:et,replace:Oe}),oe||ie);const E=ie;E.redirectedFrom=oe;let H;return!et&&rv(r,de,ie)&&(H=nr(16,{to:E,from:de}),wn(de,de,!0,!1)),(H?Promise.resolve(H):Ze(E,de)).catch(B=>Mn(B)?Mn(B,2)?B:Wt(B):Ve(B,E,de)).then(B=>{if(B){if(Mn(B,2))return He(We({replace:Oe},j(B.to),{state:typeof B.to=="object"?We({},$e,B.to.state):$e,force:et}),oe||E)}else B=Mt(E,de,!0,Oe,$e);return Tt(E,de,B),B})}function Xe(W,oe){const ie=K(W,oe);return ie?Promise.reject(ie):Promise.resolve()}function Ze(W,oe){let ie;const[de,$e,et]=ny(W,oe);ie=to(de.reverse(),"beforeRouteLeave",W,oe);for(const k of de)k.leaveGuards.forEach(E=>{ie.push(Jn(E,W,oe))});const Oe=Xe.bind(null,W,oe);return ie.push(Oe),qi(ie).then(()=>{ie=[];for(const k of s.list())ie.push(Jn(k,W,oe));return ie.push(Oe),qi(ie)}).then(()=>{ie=to($e,"beforeRouteUpdate",W,oe);for(const k of $e)k.updateGuards.forEach(E=>{ie.push(Jn(E,W,oe))});return ie.push(Oe),qi(ie)}).then(()=>{ie=[];for(const k of W.matched)if(k.beforeEnter&&!oe.matched.includes(k))if(_n(k.beforeEnter))for(const E of k.beforeEnter)ie.push(Jn(E,W,oe));else ie.push(Jn(k.beforeEnter,W,oe));return ie.push(Oe),qi(ie)}).then(()=>(W.matched.forEach(k=>k.enterCallbacks={}),ie=to(et,"beforeRouteEnter",W,oe),ie.push(Oe),qi(ie))).then(()=>{ie=[];for(const k of o.list())ie.push(Jn(k,W,oe));return ie.push(Oe),qi(ie)}).catch(k=>Mn(k,8)?k:Promise.reject(k))}function Tt(W,oe,ie){for(const de of u.list())de(W,oe,ie)}function Mt(W,oe,ie,de,$e){const et=K(W,oe);if(et)return et;const Oe=oe===Qn,k=ji?history.state:{};ie&&(de||Oe?i.replace(W.fullPath,We({scroll:Oe&&k&&k.scroll},$e)):i.push(W.fullPath,$e)),c.value=W,wn(W,oe,ie,Oe),Wt()}let Z;function qe(){Z||(Z=i.listen((W,oe,ie)=>{if(!Pn.listening)return;const de=R(W),$e=h(de);if($e){He(We($e,{replace:!0}),de).catch(Rr);return}p=de;const et=c.value;ji&&dv(tc(et.fullPath,ie.delta),ga()),Ze(de,et).catch(Oe=>Mn(Oe,12)?Oe:Mn(Oe,2)?(He(Oe.to,de).then(k=>{Mn(k,20)&&!ie.delta&&ie.type===Ur.pop&&i.go(-1,!1)}).catch(Rr),Promise.reject()):(ie.delta&&i.go(-ie.delta,!1),Ve(Oe,de,et))).then(Oe=>{Oe=Oe||Mt(de,et,!1),Oe&&(ie.delta&&!Mn(Oe,8)?i.go(-ie.delta,!1):ie.type===Ur.pop&&Mn(Oe,20)&&i.go(-1,!1)),Tt(de,et,Oe)}).catch(Rr)}))}let dt=wr(),Dt=wr(),ht;function Ve(W,oe,ie){Wt(W);const de=Dt.list();return de.length?de.forEach($e=>$e(W,oe,ie)):console.error(W),Promise.reject(W)}function Be(){return ht&&c.value!==Qn?Promise.resolve():new Promise((W,oe)=>{dt.add([W,oe])})}function Wt(W){return ht||(ht=!W,qe(),dt.list().forEach(([oe,ie])=>W?ie(W):oe()),dt.reset()),W}function wn(W,oe,ie,de){const{scrollBehavior:$e}=e;if(!ji||!$e)return Promise.resolve();const et=!ie&&hv(tc(W.fullPath,0))||(de||!ie)&&history.state&&history.state.scroll||null;return Hf().then(()=>$e(W,oe,et)).then(Oe=>Oe&&fv(Oe)).catch(Oe=>Ve(Oe,W,oe))}const pt=W=>i.go(W);let Ot;const Vt=new Set,Pn={currentRoute:c,listening:!0,addRoute:O,removeRoute:N,hasRoute:ee,getRoutes:D,resolve:R,options:e,push:ae,replace:ke,go:pt,back:()=>pt(-1),forward:()=>pt(1),beforeEach:s.add,beforeResolve:o.add,afterEach:u.add,onError:Dt.add,isReady:Be,install(W){const oe=this;W.component("RouterLink",Gv),W.component("RouterView",ey),W.config.globalProperties.$router=oe,Object.defineProperty(W.config.globalProperties,"$route",{enumerable:!0,get:()=>ri(c)}),ji&&!Ot&&c.value===Qn&&(Ot=!0,ae(i.location).catch($e=>{}));const ie={};for(const $e in Qn)ie[$e]=st(()=>c.value[$e]);W.provide(va,oe),W.provide(ml,or(ie)),W.provide(Ao,c);const de=W.unmount;Vt.add(W),W.unmount=function(){Vt.delete(W),Vt.size<1&&(p=Qn,Z&&Z(),Z=null,c.value=Qn,Ot=!1,ht=!1),de()}}};return Pn}function qi(e){return e.reduce((t,n)=>t.then(()=>n()),Promise.resolve())}function ny(e,t){const n=[],r=[],i=[],s=Math.max(t.matched.length,e.matched.length);for(let o=0;otr(p,u))?r.push(u):n.push(u));const c=e.matched[o];c&&(t.matched.find(p=>tr(p,c))||i.push(c))}return[n,r,i]}function iy(){return yn(va)}function ry(){return yn(ml)}var sy=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},bd={exports:{}};/*! + * jQuery JavaScript Library v3.6.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2022-08-26T17:52Z + */(function(e){(function(t,n){e.exports=t.document?n(t,!0):function(r){if(!r.document)throw new Error("jQuery requires a window with a document");return n(r)}})(typeof window<"u"?window:sy,function(t,n){var r=[],i=Object.getPrototypeOf,s=r.slice,o=r.flat?function(a){return r.flat.call(a)}:function(a){return r.concat.apply([],a)},u=r.push,c=r.indexOf,p={},m=p.toString,w=p.hasOwnProperty,C=w.toString,O=C.call(Object),N={},D=function(l){return typeof l=="function"&&typeof l.nodeType!="number"&&typeof l.item!="function"},ee=function(l){return l!=null&&l===l.window},R=t.document,j={type:!0,src:!0,nonce:!0,noModule:!0};function K(a,l,f){f=f||R;var d,g,v=f.createElement("script");if(v.text=a,l)for(d in j)g=l[d]||l.getAttribute&&l.getAttribute(d),g&&v.setAttribute(d,g);f.head.appendChild(v).parentNode.removeChild(v)}function ae(a){return a==null?a+"":typeof a=="object"||typeof a=="function"?p[m.call(a)]||"object":typeof a}var ke="3.6.1",h=function(a,l){return new h.fn.init(a,l)};h.fn=h.prototype={jquery:ke,constructor:h,length:0,toArray:function(){return s.call(this)},get:function(a){return a==null?s.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var l=h.merge(this.constructor(),a);return l.prevObject=this,l},each:function(a){return h.each(this,a)},map:function(a){return this.pushStack(h.map(this,function(l,f){return a.call(l,f,l)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(h.grep(this,function(a,l){return(l+1)%2}))},odd:function(){return this.pushStack(h.grep(this,function(a,l){return l%2}))},eq:function(a){var l=this.length,f=+a+(a<0?l:0);return this.pushStack(f>=0&&f0&&l-1 in a}var Xe=function(a){var l,f,d,g,v,b,A,x,P,M,V,I,F,he,xe,pe,kt,bt,Yt,Ke="sizzle"+1*new Date,Ce=a.document,qt=0,Ne=0,ft=us(),mr=us(),as=us(),Kt=us(),gi=function(_,T){return _===T&&(V=!0),0},vi={}.hasOwnProperty,Bt=[],Vn=Bt.pop,tn=Bt.push,Yn=Bt.push,vu=Bt.slice,yi=function(_,T){for(var S=0,q=_.length;S+~]|"+je+")"+je+"*"),Cp=new RegExp(je+"|>"),xp=new RegExp(Ha),$p=new RegExp("^"+bi+"$"),ls={ID:new RegExp("^#("+bi+")"),CLASS:new RegExp("^\\.("+bi+")"),TAG:new RegExp("^("+bi+"|[*])"),ATTR:new RegExp("^"+yu),PSEUDO:new RegExp("^"+Ha),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+je+"*(even|odd|(([+-]|)(\\d*)n|)"+je+"*(?:([+-]|)"+je+"*(\\d+)|))"+je+"*\\)|)","i"),bool:new RegExp("^(?:"+Da+")$","i"),needsContext:new RegExp("^"+je+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+je+"*((?:-\\d)?\\d*)"+je+"*\\)|)(?=[^-]|$)","i")},Tp=/HTML$/i,Ap=/^(?:input|select|textarea|button)$/i,Ep=/^h\d$/i,gr=/^[^{]+\{\s*\[native \w/,Sp=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Fa=/[+~]/,In=new RegExp("\\\\[\\da-fA-F]{1,6}"+je+"?|\\\\([^\\r\\n\\f])","g"),Nn=function(_,T){var S="0x"+_.slice(1)-65536;return T||(S<0?String.fromCharCode(S+65536):String.fromCharCode(S>>10|55296,S&1023|56320))},_u=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,wu=function(_,T){return T?_==="\0"?"\uFFFD":_.slice(0,-1)+"\\"+_.charCodeAt(_.length-1).toString(16)+" ":"\\"+_},ku=function(){I()},Op=fs(function(_){return _.disabled===!0&&_.nodeName.toLowerCase()==="fieldset"},{dir:"parentNode",next:"legend"});try{Yn.apply(Bt=vu.call(Ce.childNodes),Ce.childNodes),Bt[Ce.childNodes.length].nodeType}catch{Yn={apply:Bt.length?function(T,S){tn.apply(T,vu.call(S))}:function(T,S){for(var q=T.length,z=0;T[q++]=S[z++];);T.length=q-1}}}function Qe(_,T,S,q){var z,U,Y,te,re,ye,ge,_e=T&&T.ownerDocument,Pe=T?T.nodeType:9;if(S=S||[],typeof _!="string"||!_||Pe!==1&&Pe!==9&&Pe!==11)return S;if(!q&&(I(T),T=T||F,xe)){if(Pe!==11&&(re=Sp.exec(_)))if(z=re[1]){if(Pe===9)if(Y=T.getElementById(z)){if(Y.id===z)return S.push(Y),S}else return S;else if(_e&&(Y=_e.getElementById(z))&&Yt(T,Y)&&Y.id===z)return S.push(Y),S}else{if(re[2])return Yn.apply(S,T.getElementsByTagName(_)),S;if((z=re[3])&&f.getElementsByClassName&&T.getElementsByClassName)return Yn.apply(S,T.getElementsByClassName(z)),S}if(f.qsa&&!Kt[_+" "]&&(!pe||!pe.test(_))&&(Pe!==1||T.nodeName.toLowerCase()!=="object")){if(ge=_,_e=T,Pe===1&&(Cp.test(_)||bu.test(_))){for(_e=Fa.test(_)&&Ba(T.parentNode)||T,(_e!==T||!f.scope)&&((te=T.getAttribute("id"))?te=te.replace(_u,wu):T.setAttribute("id",te=Ke)),ye=b(_),U=ye.length;U--;)ye[U]=(te?"#"+te:":scope")+" "+cs(ye[U]);ge=ye.join(",")}try{return Yn.apply(S,_e.querySelectorAll(ge)),S}catch{Kt(_,!0)}finally{te===Ke&&T.removeAttribute("id")}}}return x(_.replace(os,"$1"),T,S,q)}function us(){var _=[];function T(S,q){return _.push(S+" ")>d.cacheLength&&delete T[_.shift()],T[S+" "]=q}return T}function un(_){return _[Ke]=!0,_}function cn(_){var T=F.createElement("fieldset");try{return!!_(T)}catch{return!1}finally{T.parentNode&&T.parentNode.removeChild(T),T=null}}function qa(_,T){for(var S=_.split("|"),q=S.length;q--;)d.attrHandle[S[q]]=T}function Cu(_,T){var S=T&&_,q=S&&_.nodeType===1&&T.nodeType===1&&_.sourceIndex-T.sourceIndex;if(q)return q;if(S){for(;S=S.nextSibling;)if(S===T)return-1}return _?1:-1}function Rp(_){return function(T){var S=T.nodeName.toLowerCase();return S==="input"&&T.type===_}}function zp(_){return function(T){var S=T.nodeName.toLowerCase();return(S==="input"||S==="button")&&T.type===_}}function xu(_){return function(T){return"form"in T?T.parentNode&&T.disabled===!1?"label"in T?"label"in T.parentNode?T.parentNode.disabled===_:T.disabled===_:T.isDisabled===_||T.isDisabled!==!_&&Op(T)===_:T.disabled===_:"label"in T?T.disabled===_:!1}}function _i(_){return un(function(T){return T=+T,un(function(S,q){for(var z,U=_([],S.length,T),Y=U.length;Y--;)S[z=U[Y]]&&(S[z]=!(q[z]=S[z]))})})}function Ba(_){return _&&typeof _.getElementsByTagName<"u"&&_}f=Qe.support={},v=Qe.isXML=function(_){var T=_&&_.namespaceURI,S=_&&(_.ownerDocument||_).documentElement;return!Tp.test(T||S&&S.nodeName||"HTML")},I=Qe.setDocument=function(_){var T,S,q=_?_.ownerDocument||_:Ce;return q==F||q.nodeType!==9||!q.documentElement||(F=q,he=F.documentElement,xe=!v(F),Ce!=F&&(S=F.defaultView)&&S.top!==S&&(S.addEventListener?S.addEventListener("unload",ku,!1):S.attachEvent&&S.attachEvent("onunload",ku)),f.scope=cn(function(z){return he.appendChild(z).appendChild(F.createElement("div")),typeof z.querySelectorAll<"u"&&!z.querySelectorAll(":scope fieldset div").length}),f.attributes=cn(function(z){return z.className="i",!z.getAttribute("className")}),f.getElementsByTagName=cn(function(z){return z.appendChild(F.createComment("")),!z.getElementsByTagName("*").length}),f.getElementsByClassName=gr.test(F.getElementsByClassName),f.getById=cn(function(z){return he.appendChild(z).id=Ke,!F.getElementsByName||!F.getElementsByName(Ke).length}),f.getById?(d.filter.ID=function(z){var U=z.replace(In,Nn);return function(Y){return Y.getAttribute("id")===U}},d.find.ID=function(z,U){if(typeof U.getElementById<"u"&&xe){var Y=U.getElementById(z);return Y?[Y]:[]}}):(d.filter.ID=function(z){var U=z.replace(In,Nn);return function(Y){var te=typeof Y.getAttributeNode<"u"&&Y.getAttributeNode("id");return te&&te.value===U}},d.find.ID=function(z,U){if(typeof U.getElementById<"u"&&xe){var Y,te,re,ye=U.getElementById(z);if(ye){if(Y=ye.getAttributeNode("id"),Y&&Y.value===z)return[ye];for(re=U.getElementsByName(z),te=0;ye=re[te++];)if(Y=ye.getAttributeNode("id"),Y&&Y.value===z)return[ye]}return[]}}),d.find.TAG=f.getElementsByTagName?function(z,U){if(typeof U.getElementsByTagName<"u")return U.getElementsByTagName(z);if(f.qsa)return U.querySelectorAll(z)}:function(z,U){var Y,te=[],re=0,ye=U.getElementsByTagName(z);if(z==="*"){for(;Y=ye[re++];)Y.nodeType===1&&te.push(Y);return te}return ye},d.find.CLASS=f.getElementsByClassName&&function(z,U){if(typeof U.getElementsByClassName<"u"&&xe)return U.getElementsByClassName(z)},kt=[],pe=[],(f.qsa=gr.test(F.querySelectorAll))&&(cn(function(z){var U;he.appendChild(z).innerHTML="",z.querySelectorAll("[msallowcapture^='']").length&&pe.push("[*^$]="+je+`*(?:''|"")`),z.querySelectorAll("[selected]").length||pe.push("\\["+je+"*(?:value|"+Da+")"),z.querySelectorAll("[id~="+Ke+"-]").length||pe.push("~="),U=F.createElement("input"),U.setAttribute("name",""),z.appendChild(U),z.querySelectorAll("[name='']").length||pe.push("\\["+je+"*name"+je+"*="+je+`*(?:''|"")`),z.querySelectorAll(":checked").length||pe.push(":checked"),z.querySelectorAll("a#"+Ke+"+*").length||pe.push(".#.+[+~]"),z.querySelectorAll("\\\f"),pe.push("[\\r\\n\\f]")}),cn(function(z){z.innerHTML="";var U=F.createElement("input");U.setAttribute("type","hidden"),z.appendChild(U).setAttribute("name","D"),z.querySelectorAll("[name=d]").length&&pe.push("name"+je+"*[*^$|!~]?="),z.querySelectorAll(":enabled").length!==2&&pe.push(":enabled",":disabled"),he.appendChild(z).disabled=!0,z.querySelectorAll(":disabled").length!==2&&pe.push(":enabled",":disabled"),z.querySelectorAll("*,:x"),pe.push(",.*:")})),(f.matchesSelector=gr.test(bt=he.matches||he.webkitMatchesSelector||he.mozMatchesSelector||he.oMatchesSelector||he.msMatchesSelector))&&cn(function(z){f.disconnectedMatch=bt.call(z,"*"),bt.call(z,"[s!='']:x"),kt.push("!=",Ha)}),pe=pe.length&&new RegExp(pe.join("|")),kt=kt.length&&new RegExp(kt.join("|")),T=gr.test(he.compareDocumentPosition),Yt=T||gr.test(he.contains)?function(z,U){var Y=z.nodeType===9?z.documentElement:z,te=U&&U.parentNode;return z===te||!!(te&&te.nodeType===1&&(Y.contains?Y.contains(te):z.compareDocumentPosition&&z.compareDocumentPosition(te)&16))}:function(z,U){if(U){for(;U=U.parentNode;)if(U===z)return!0}return!1},gi=T?function(z,U){if(z===U)return V=!0,0;var Y=!z.compareDocumentPosition-!U.compareDocumentPosition;return Y||(Y=(z.ownerDocument||z)==(U.ownerDocument||U)?z.compareDocumentPosition(U):1,Y&1||!f.sortDetached&&U.compareDocumentPosition(z)===Y?z==F||z.ownerDocument==Ce&&Yt(Ce,z)?-1:U==F||U.ownerDocument==Ce&&Yt(Ce,U)?1:M?yi(M,z)-yi(M,U):0:Y&4?-1:1)}:function(z,U){if(z===U)return V=!0,0;var Y,te=0,re=z.parentNode,ye=U.parentNode,ge=[z],_e=[U];if(!re||!ye)return z==F?-1:U==F?1:re?-1:ye?1:M?yi(M,z)-yi(M,U):0;if(re===ye)return Cu(z,U);for(Y=z;Y=Y.parentNode;)ge.unshift(Y);for(Y=U;Y=Y.parentNode;)_e.unshift(Y);for(;ge[te]===_e[te];)te++;return te?Cu(ge[te],_e[te]):ge[te]==Ce?-1:_e[te]==Ce?1:0}),F},Qe.matches=function(_,T){return Qe(_,null,null,T)},Qe.matchesSelector=function(_,T){if(I(_),f.matchesSelector&&xe&&!Kt[T+" "]&&(!kt||!kt.test(T))&&(!pe||!pe.test(T)))try{var S=bt.call(_,T);if(S||f.disconnectedMatch||_.document&&_.document.nodeType!==11)return S}catch{Kt(T,!0)}return Qe(T,F,null,[_]).length>0},Qe.contains=function(_,T){return(_.ownerDocument||_)!=F&&I(_),Yt(_,T)},Qe.attr=function(_,T){(_.ownerDocument||_)!=F&&I(_);var S=d.attrHandle[T.toLowerCase()],q=S&&vi.call(d.attrHandle,T.toLowerCase())?S(_,T,!xe):void 0;return q!==void 0?q:f.attributes||!xe?_.getAttribute(T):(q=_.getAttributeNode(T))&&q.specified?q.value:null},Qe.escape=function(_){return(_+"").replace(_u,wu)},Qe.error=function(_){throw new Error("Syntax error, unrecognized expression: "+_)},Qe.uniqueSort=function(_){var T,S=[],q=0,z=0;if(V=!f.detectDuplicates,M=!f.sortStable&&_.slice(0),_.sort(gi),V){for(;T=_[z++];)T===_[z]&&(q=S.push(z));for(;q--;)_.splice(S[q],1)}return M=null,_},g=Qe.getText=function(_){var T,S="",q=0,z=_.nodeType;if(z){if(z===1||z===9||z===11){if(typeof _.textContent=="string")return _.textContent;for(_=_.firstChild;_;_=_.nextSibling)S+=g(_)}else if(z===3||z===4)return _.nodeValue}else for(;T=_[q++];)S+=g(T);return S},d=Qe.selectors={cacheLength:50,createPseudo:un,match:ls,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(_){return _[1]=_[1].replace(In,Nn),_[3]=(_[3]||_[4]||_[5]||"").replace(In,Nn),_[2]==="~="&&(_[3]=" "+_[3]+" "),_.slice(0,4)},CHILD:function(_){return _[1]=_[1].toLowerCase(),_[1].slice(0,3)==="nth"?(_[3]||Qe.error(_[0]),_[4]=+(_[4]?_[5]+(_[6]||1):2*(_[3]==="even"||_[3]==="odd")),_[5]=+(_[7]+_[8]||_[3]==="odd")):_[3]&&Qe.error(_[0]),_},PSEUDO:function(_){var T,S=!_[6]&&_[2];return ls.CHILD.test(_[0])?null:(_[3]?_[2]=_[4]||_[5]||"":S&&xp.test(S)&&(T=b(S,!0))&&(T=S.indexOf(")",S.length-T)-S.length)&&(_[0]=_[0].slice(0,T),_[2]=S.slice(0,T)),_.slice(0,3))}},filter:{TAG:function(_){var T=_.replace(In,Nn).toLowerCase();return _==="*"?function(){return!0}:function(S){return S.nodeName&&S.nodeName.toLowerCase()===T}},CLASS:function(_){var T=ft[_+" "];return T||(T=new RegExp("(^|"+je+")"+_+"("+je+"|$)"))&&ft(_,function(S){return T.test(typeof S.className=="string"&&S.className||typeof S.getAttribute<"u"&&S.getAttribute("class")||"")})},ATTR:function(_,T,S){return function(q){var z=Qe.attr(q,_);return z==null?T==="!=":T?(z+="",T==="="?z===S:T==="!="?z!==S:T==="^="?S&&z.indexOf(S)===0:T==="*="?S&&z.indexOf(S)>-1:T==="$="?S&&z.slice(-S.length)===S:T==="~="?(" "+z.replace(wp," ")+" ").indexOf(S)>-1:T==="|="?z===S||z.slice(0,S.length+1)===S+"-":!1):!0}},CHILD:function(_,T,S,q,z){var U=_.slice(0,3)!=="nth",Y=_.slice(-4)!=="last",te=T==="of-type";return q===1&&z===0?function(re){return!!re.parentNode}:function(re,ye,ge){var _e,Pe,Ge,be,Ct,zt,Qt=U!==Y?"nextSibling":"previousSibling",rt=re.parentNode,vr=te&&re.nodeName.toLowerCase(),yr=!ge&&!te,Gt=!1;if(rt){if(U){for(;Qt;){for(be=re;be=be[Qt];)if(te?be.nodeName.toLowerCase()===vr:be.nodeType===1)return!1;zt=Qt=_==="only"&&!zt&&"nextSibling"}return!0}if(zt=[Y?rt.firstChild:rt.lastChild],Y&&yr){for(be=rt,Ge=be[Ke]||(be[Ke]={}),Pe=Ge[be.uniqueID]||(Ge[be.uniqueID]={}),_e=Pe[_]||[],Ct=_e[0]===qt&&_e[1],Gt=Ct&&_e[2],be=Ct&&rt.childNodes[Ct];be=++Ct&&be&&be[Qt]||(Gt=Ct=0)||zt.pop();)if(be.nodeType===1&&++Gt&&be===re){Pe[_]=[qt,Ct,Gt];break}}else if(yr&&(be=re,Ge=be[Ke]||(be[Ke]={}),Pe=Ge[be.uniqueID]||(Ge[be.uniqueID]={}),_e=Pe[_]||[],Ct=_e[0]===qt&&_e[1],Gt=Ct),Gt===!1)for(;(be=++Ct&&be&&be[Qt]||(Gt=Ct=0)||zt.pop())&&!((te?be.nodeName.toLowerCase()===vr:be.nodeType===1)&&++Gt&&(yr&&(Ge=be[Ke]||(be[Ke]={}),Pe=Ge[be.uniqueID]||(Ge[be.uniqueID]={}),Pe[_]=[qt,Gt]),be===re)););return Gt-=z,Gt===q||Gt%q===0&&Gt/q>=0}}},PSEUDO:function(_,T){var S,q=d.pseudos[_]||d.setFilters[_.toLowerCase()]||Qe.error("unsupported pseudo: "+_);return q[Ke]?q(T):q.length>1?(S=[_,_,"",T],d.setFilters.hasOwnProperty(_.toLowerCase())?un(function(z,U){for(var Y,te=q(z,T),re=te.length;re--;)Y=yi(z,te[re]),z[Y]=!(U[Y]=te[re])}):function(z){return q(z,0,S)}):q}},pseudos:{not:un(function(_){var T=[],S=[],q=A(_.replace(os,"$1"));return q[Ke]?un(function(z,U,Y,te){for(var re,ye=q(z,null,te,[]),ge=z.length;ge--;)(re=ye[ge])&&(z[ge]=!(U[ge]=re))}):function(z,U,Y){return T[0]=z,q(T,null,Y,S),T[0]=null,!S.pop()}}),has:un(function(_){return function(T){return Qe(_,T).length>0}}),contains:un(function(_){return _=_.replace(In,Nn),function(T){return(T.textContent||g(T)).indexOf(_)>-1}}),lang:un(function(_){return $p.test(_||"")||Qe.error("unsupported lang: "+_),_=_.replace(In,Nn).toLowerCase(),function(T){var S;do if(S=xe?T.lang:T.getAttribute("xml:lang")||T.getAttribute("lang"))return S=S.toLowerCase(),S===_||S.indexOf(_+"-")===0;while((T=T.parentNode)&&T.nodeType===1);return!1}}),target:function(_){var T=a.location&&a.location.hash;return T&&T.slice(1)===_.id},root:function(_){return _===he},focus:function(_){return _===F.activeElement&&(!F.hasFocus||F.hasFocus())&&!!(_.type||_.href||~_.tabIndex)},enabled:xu(!1),disabled:xu(!0),checked:function(_){var T=_.nodeName.toLowerCase();return T==="input"&&!!_.checked||T==="option"&&!!_.selected},selected:function(_){return _.parentNode&&_.parentNode.selectedIndex,_.selected===!0},empty:function(_){for(_=_.firstChild;_;_=_.nextSibling)if(_.nodeType<6)return!1;return!0},parent:function(_){return!d.pseudos.empty(_)},header:function(_){return Ep.test(_.nodeName)},input:function(_){return Ap.test(_.nodeName)},button:function(_){var T=_.nodeName.toLowerCase();return T==="input"&&_.type==="button"||T==="button"},text:function(_){var T;return _.nodeName.toLowerCase()==="input"&&_.type==="text"&&((T=_.getAttribute("type"))==null||T.toLowerCase()==="text")},first:_i(function(){return[0]}),last:_i(function(_,T){return[T-1]}),eq:_i(function(_,T,S){return[S<0?S+T:S]}),even:_i(function(_,T){for(var S=0;ST?T:S;--q>=0;)_.push(q);return _}),gt:_i(function(_,T,S){for(var q=S<0?S+T:S;++q1?function(T,S,q){for(var z=_.length;z--;)if(!_[z](T,S,q))return!1;return!0}:_[0]}function Pp(_,T,S){for(var q=0,z=T.length;q-1&&(Y[ge]=!(te[ge]=Pe))}}else rt=ds(rt===te?rt.splice(Ct,rt.length):rt),z?z(null,te,rt,ye):Yn.apply(te,rt)})}function Wa(_){for(var T,S,q,z=_.length,U=d.relative[_[0].type],Y=U||d.relative[" "],te=U?1:0,re=fs(function(_e){return _e===T},Y,!0),ye=fs(function(_e){return yi(T,_e)>-1},Y,!0),ge=[function(_e,Pe,Ge){var be=!U&&(Ge||Pe!==P)||((T=Pe).nodeType?re(_e,Pe,Ge):ye(_e,Pe,Ge));return T=null,be}];te1&&ja(ge),te>1&&cs(_.slice(0,te-1).concat({value:_[te-2].type===" "?"*":""})).replace(os,"$1"),S,te0,q=_.length>0,z=function(U,Y,te,re,ye){var ge,_e,Pe,Ge=0,be="0",Ct=U&&[],zt=[],Qt=P,rt=U||q&&d.find.TAG("*",ye),vr=qt+=Qt==null?1:Math.random()||.1,yr=rt.length;for(ye&&(P=Y==F||Y||ye);be!==yr&&(ge=rt[be])!=null;be++){if(q&&ge){for(_e=0,!Y&&ge.ownerDocument!=F&&(I(ge),te=!xe);Pe=_[_e++];)if(Pe(ge,Y||F,te)){re.push(ge);break}ye&&(qt=vr)}S&&((ge=!Pe&&ge)&&Ge--,U&&Ct.push(ge))}if(Ge+=be,S&&be!==Ge){for(_e=0;Pe=T[_e++];)Pe(Ct,zt,Y,te);if(U){if(Ge>0)for(;be--;)Ct[be]||zt[be]||(zt[be]=Vn.call(re));zt=ds(zt)}Yn.apply(re,zt),ye&&!U&&zt.length>0&&Ge+T.length>1&&Qe.uniqueSort(re)}return ye&&(qt=vr,P=Qt),Ct};return S?un(z):z}return A=Qe.compile=function(_,T){var S,q=[],z=[],U=as[_+" "];if(!U){for(T||(T=b(_)),S=T.length;S--;)U=Wa(T[S]),U[Ke]?q.push(U):z.push(U);U=as(_,Lp(z,q)),U.selector=_}return U},x=Qe.select=function(_,T,S,q){var z,U,Y,te,re,ye=typeof _=="function"&&_,ge=!q&&b(_=ye.selector||_);if(S=S||[],ge.length===1){if(U=ge[0]=ge[0].slice(0),U.length>2&&(Y=U[0]).type==="ID"&&T.nodeType===9&&xe&&d.relative[U[1].type]){if(T=(d.find.ID(Y.matches[0].replace(In,Nn),T)||[])[0],T)ye&&(T=T.parentNode);else return S;_=_.slice(U.shift().value.length)}for(z=ls.needsContext.test(_)?0:U.length;z--&&(Y=U[z],!d.relative[te=Y.type]);)if((re=d.find[te])&&(q=re(Y.matches[0].replace(In,Nn),Fa.test(U[0].type)&&Ba(T.parentNode)||T))){if(U.splice(z,1),_=q.length&&cs(U),!_)return Yn.apply(S,q),S;break}}return(ye||A(_,ge))(q,T,!xe,S,!T||Fa.test(_)&&Ba(T.parentNode)||T),S},f.sortStable=Ke.split("").sort(gi).join("")===Ke,f.detectDuplicates=!!V,I(),f.sortDetached=cn(function(_){return _.compareDocumentPosition(F.createElement("fieldset"))&1}),cn(function(_){return _.innerHTML="",_.firstChild.getAttribute("href")==="#"})||qa("type|href|height|width",function(_,T,S){if(!S)return _.getAttribute(T,T.toLowerCase()==="type"?1:2)}),(!f.attributes||!cn(function(_){return _.innerHTML="",_.firstChild.setAttribute("value",""),_.firstChild.getAttribute("value")===""}))&&qa("value",function(_,T,S){if(!S&&_.nodeName.toLowerCase()==="input")return _.defaultValue}),cn(function(_){return _.getAttribute("disabled")==null})||qa(Da,function(_,T,S){var q;if(!S)return _[T]===!0?T.toLowerCase():(q=_.getAttributeNode(T))&&q.specified?q.value:null}),Qe}(t);h.find=Xe,h.expr=Xe.selectors,h.expr[":"]=h.expr.pseudos,h.uniqueSort=h.unique=Xe.uniqueSort,h.text=Xe.getText,h.isXMLDoc=Xe.isXML,h.contains=Xe.contains,h.escapeSelector=Xe.escape;var Ze=function(a,l,f){for(var d=[],g=f!==void 0;(a=a[l])&&a.nodeType!==9;)if(a.nodeType===1){if(g&&h(a).is(f))break;d.push(a)}return d},Tt=function(a,l){for(var f=[];a;a=a.nextSibling)a.nodeType===1&&a!==l&&f.push(a);return f},Mt=h.expr.match.needsContext;function Z(a,l){return a.nodeName&&a.nodeName.toLowerCase()===l.toLowerCase()}var qe=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function dt(a,l,f){return D(l)?h.grep(a,function(d,g){return!!l.call(d,g,d)!==f}):l.nodeType?h.grep(a,function(d){return d===l!==f}):typeof l!="string"?h.grep(a,function(d){return c.call(l,d)>-1!==f}):h.filter(l,a,f)}h.filter=function(a,l,f){var d=l[0];return f&&(a=":not("+a+")"),l.length===1&&d.nodeType===1?h.find.matchesSelector(d,a)?[d]:[]:h.find.matches(a,h.grep(l,function(g){return g.nodeType===1}))},h.fn.extend({find:function(a){var l,f,d=this.length,g=this;if(typeof a!="string")return this.pushStack(h(a).filter(function(){for(l=0;l1?h.uniqueSort(f):f},filter:function(a){return this.pushStack(dt(this,a||[],!1))},not:function(a){return this.pushStack(dt(this,a||[],!0))},is:function(a){return!!dt(this,typeof a=="string"&&Mt.test(a)?h(a):a||[],!1).length}});var Dt,ht=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,Ve=h.fn.init=function(a,l,f){var d,g;if(!a)return this;if(f=f||Dt,typeof a=="string")if(a[0]==="<"&&a[a.length-1]===">"&&a.length>=3?d=[null,a,null]:d=ht.exec(a),d&&(d[1]||!l))if(d[1]){if(l=l instanceof h?l[0]:l,h.merge(this,h.parseHTML(d[1],l&&l.nodeType?l.ownerDocument||l:R,!0)),qe.test(d[1])&&h.isPlainObject(l))for(d in l)D(this[d])?this[d](l[d]):this.attr(d,l[d]);return this}else return g=R.getElementById(d[2]),g&&(this[0]=g,this.length=1),this;else return!l||l.jquery?(l||f).find(a):this.constructor(l).find(a);else{if(a.nodeType)return this[0]=a,this.length=1,this;if(D(a))return f.ready!==void 0?f.ready(a):a(h)}return h.makeArray(a,this)};Ve.prototype=h.fn,Dt=h(R);var Be=/^(?:parents|prev(?:Until|All))/,Wt={children:!0,contents:!0,next:!0,prev:!0};h.fn.extend({has:function(a){var l=h(a,this),f=l.length;return this.filter(function(){for(var d=0;d-1:f.nodeType===1&&h.find.matchesSelector(f,a))){v.push(f);break}}return this.pushStack(v.length>1?h.uniqueSort(v):v)},index:function(a){return a?typeof a=="string"?c.call(h(a),this[0]):c.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,l){return this.pushStack(h.uniqueSort(h.merge(this.get(),h(a,l))))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}});function wn(a,l){for(;(a=a[l])&&a.nodeType!==1;);return a}h.each({parent:function(a){var l=a.parentNode;return l&&l.nodeType!==11?l:null},parents:function(a){return Ze(a,"parentNode")},parentsUntil:function(a,l,f){return Ze(a,"parentNode",f)},next:function(a){return wn(a,"nextSibling")},prev:function(a){return wn(a,"previousSibling")},nextAll:function(a){return Ze(a,"nextSibling")},prevAll:function(a){return Ze(a,"previousSibling")},nextUntil:function(a,l,f){return Ze(a,"nextSibling",f)},prevUntil:function(a,l,f){return Ze(a,"previousSibling",f)},siblings:function(a){return Tt((a.parentNode||{}).firstChild,a)},children:function(a){return Tt(a.firstChild)},contents:function(a){return a.contentDocument!=null&&i(a.contentDocument)?a.contentDocument:(Z(a,"template")&&(a=a.content||a),h.merge([],a.childNodes))}},function(a,l){h.fn[a]=function(f,d){var g=h.map(this,l,f);return a.slice(-5)!=="Until"&&(d=f),d&&typeof d=="string"&&(g=h.filter(d,g)),this.length>1&&(Wt[a]||h.uniqueSort(g),Be.test(a)&&g.reverse()),this.pushStack(g)}});var pt=/[^\x20\t\r\n\f]+/g;function Ot(a){var l={};return h.each(a.match(pt)||[],function(f,d){l[d]=!0}),l}h.Callbacks=function(a){a=typeof a=="string"?Ot(a):h.extend({},a);var l,f,d,g,v=[],b=[],A=-1,x=function(){for(g=g||a.once,d=l=!0;b.length;A=-1)for(f=b.shift();++A-1;)v.splice(I,1),I<=A&&A--}),this},has:function(M){return M?h.inArray(M,v)>-1:v.length>0},empty:function(){return v&&(v=[]),this},disable:function(){return g=b=[],v=f="",this},disabled:function(){return!v},lock:function(){return g=b=[],!f&&!l&&(v=f=""),this},locked:function(){return!!g},fireWith:function(M,V){return g||(V=V||[],V=[M,V.slice?V.slice():V],b.push(V),l||x()),this},fire:function(){return P.fireWith(this,arguments),this},fired:function(){return!!d}};return P};function Vt(a){return a}function Pn(a){throw a}function W(a,l,f,d){var g;try{a&&D(g=a.promise)?g.call(a).done(l).fail(f):a&&D(g=a.then)?g.call(a,l,f):l.apply(void 0,[a].slice(d))}catch(v){f.apply(void 0,[v])}}h.extend({Deferred:function(a){var l=[["notify","progress",h.Callbacks("memory"),h.Callbacks("memory"),2],["resolve","done",h.Callbacks("once memory"),h.Callbacks("once memory"),0,"resolved"],["reject","fail",h.Callbacks("once memory"),h.Callbacks("once memory"),1,"rejected"]],f="pending",d={state:function(){return f},always:function(){return g.done(arguments).fail(arguments),this},catch:function(v){return d.then(null,v)},pipe:function(){var v=arguments;return h.Deferred(function(b){h.each(l,function(A,x){var P=D(v[x[4]])&&v[x[4]];g[x[1]](function(){var M=P&&P.apply(this,arguments);M&&D(M.promise)?M.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[x[0]+"With"](this,P?[M]:arguments)})}),v=null}).promise()},then:function(v,b,A){var x=0;function P(M,V,I,F){return function(){var he=this,xe=arguments,pe=function(){var bt,Yt;if(!(M=x&&(I!==Pn&&(he=void 0,xe=[bt]),V.rejectWith(he,xe))}};M?kt():(h.Deferred.getStackHook&&(kt.stackTrace=h.Deferred.getStackHook()),t.setTimeout(kt))}}return h.Deferred(function(M){l[0][3].add(P(0,M,D(A)?A:Vt,M.notifyWith)),l[1][3].add(P(0,M,D(v)?v:Vt)),l[2][3].add(P(0,M,D(b)?b:Pn))}).promise()},promise:function(v){return v!=null?h.extend(v,d):d}},g={};return h.each(l,function(v,b){var A=b[2],x=b[5];d[b[1]]=A.add,x&&A.add(function(){f=x},l[3-v][2].disable,l[3-v][3].disable,l[0][2].lock,l[0][3].lock),A.add(b[3].fire),g[b[0]]=function(){return g[b[0]+"With"](this===g?void 0:this,arguments),this},g[b[0]+"With"]=A.fireWith}),d.promise(g),a&&a.call(g,g),g},when:function(a){var l=arguments.length,f=l,d=Array(f),g=s.call(arguments),v=h.Deferred(),b=function(A){return function(x){d[A]=this,g[A]=arguments.length>1?s.call(arguments):x,--l||v.resolveWith(d,g)}};if(l<=1&&(W(a,v.done(b(f)).resolve,v.reject,!l),v.state()==="pending"||D(g[f]&&g[f].then)))return v.then();for(;f--;)W(g[f],b(f),v.reject);return v.promise()}});var oe=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;h.Deferred.exceptionHook=function(a,l){t.console&&t.console.warn&&a&&oe.test(a.name)&&t.console.warn("jQuery.Deferred exception: "+a.message,a.stack,l)},h.readyException=function(a){t.setTimeout(function(){throw a})};var ie=h.Deferred();h.fn.ready=function(a){return ie.then(a).catch(function(l){h.readyException(l)}),this},h.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--h.readyWait:h.isReady)||(h.isReady=!0,!(a!==!0&&--h.readyWait>0)&&ie.resolveWith(R,[h]))}}),h.ready.then=ie.then;function de(){R.removeEventListener("DOMContentLoaded",de),t.removeEventListener("load",de),h.ready()}R.readyState==="complete"||R.readyState!=="loading"&&!R.documentElement.doScroll?t.setTimeout(h.ready):(R.addEventListener("DOMContentLoaded",de),t.addEventListener("load",de));var $e=function(a,l,f,d,g,v,b){var A=0,x=a.length,P=f==null;if(ae(f)==="object"){g=!0;for(A in f)$e(a,l,A,f[A],!0,v,b)}else if(d!==void 0&&(g=!0,D(d)||(b=!0),P&&(b?(l.call(a,d),l=null):(P=l,l=function(M,V,I){return P.call(h(M),I)})),l))for(;A1,null,!0)},removeData:function(a){return this.each(function(){Q.remove(this,a)})}}),h.extend({queue:function(a,l,f){var d;if(a)return l=(l||"fx")+"queue",d=L.get(a,l),f&&(!d||Array.isArray(f)?d=L.access(a,l,h.makeArray(f)):d.push(f)),d||[]},dequeue:function(a,l){l=l||"fx";var f=h.queue(a,l),d=f.length,g=f.shift(),v=h._queueHooks(a,l),b=function(){h.dequeue(a,l)};g==="inprogress"&&(g=f.shift(),d--),g&&(l==="fx"&&f.unshift("inprogress"),delete v.stop,g.call(a,b,v)),!d&&v&&v.empty.fire()},_queueHooks:function(a,l){var f=l+"queueHooks";return L.get(a,f)||L.access(a,f,{empty:h.Callbacks("once memory").add(function(){L.remove(a,[l+"queue",f])})})}}),h.fn.extend({queue:function(a,l){var f=2;return typeof a!="string"&&(l=a,a="fx",f--),arguments.length\x20\t\r\n\f]*)/i,Rt=/^$|^module$|\/(?:java|ecma)script/i;(function(){var a=R.createDocumentFragment(),l=a.appendChild(R.createElement("div")),f=R.createElement("input");f.setAttribute("type","radio"),f.setAttribute("checked","checked"),f.setAttribute("name","t"),l.appendChild(f),N.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,l.innerHTML="",N.noCloneChecked=!!l.cloneNode(!0).lastChild.defaultValue,l.innerHTML="",N.option=!!l.lastChild})();var ct={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,N.option||(ct.optgroup=ct.option=[1,""]);function At(a,l){var f;return typeof a.getElementsByTagName<"u"?f=a.getElementsByTagName(l||"*"):typeof a.querySelectorAll<"u"?f=a.querySelectorAll(l||"*"):f=[],l===void 0||l&&Z(a,l)?h.merge([a],f):f}function xa(a,l){for(var f=0,d=a.length;f-1){g&&g.push(v);continue}if(P=Te(v),b=At(V.appendChild(v),"script"),P&&xa(b),f)for(M=0;v=b[M++];)Rt.test(v.type||"")&&f.push(v)}return V}var Vl=/^([^.]*)(?:\.(.+)|)/;function Mi(){return!0}function Di(){return!1}function Hh(a,l){return a===Fh()==(l==="focus")}function Fh(){try{return R.activeElement}catch{}}function $a(a,l,f,d,g,v){var b,A;if(typeof l=="object"){typeof f!="string"&&(d=d||f,f=void 0);for(A in l)$a(a,A,f,d,l[A],v);return a}if(d==null&&g==null?(g=f,d=f=void 0):g==null&&(typeof f=="string"?(g=d,d=void 0):(g=d,d=f,f=void 0)),g===!1)g=Di;else if(!g)return a;return v===1&&(b=g,g=function(x){return h().off(x),b.apply(this,arguments)},g.guid=b.guid||(b.guid=h.guid++)),a.each(function(){h.event.add(this,l,g,d,f)})}h.event={global:{},add:function(a,l,f,d,g){var v,b,A,x,P,M,V,I,F,he,xe,pe=L.get(a);if(!!H(a))for(f.handler&&(v=f,f=v.handler,g=v.selector),g&&h.find.matchesSelector(ve,g),f.guid||(f.guid=h.guid++),(x=pe.events)||(x=pe.events=Object.create(null)),(b=pe.handle)||(b=pe.handle=function(kt){return typeof h<"u"&&h.event.triggered!==kt.type?h.event.dispatch.apply(a,arguments):void 0}),l=(l||"").match(pt)||[""],P=l.length;P--;)A=Vl.exec(l[P])||[],F=xe=A[1],he=(A[2]||"").split(".").sort(),F&&(V=h.event.special[F]||{},F=(g?V.delegateType:V.bindType)||F,V=h.event.special[F]||{},M=h.extend({type:F,origType:xe,data:d,handler:f,guid:f.guid,selector:g,needsContext:g&&h.expr.match.needsContext.test(g),namespace:he.join(".")},v),(I=x[F])||(I=x[F]=[],I.delegateCount=0,(!V.setup||V.setup.call(a,d,he,b)===!1)&&a.addEventListener&&a.addEventListener(F,b)),V.add&&(V.add.call(a,M),M.handler.guid||(M.handler.guid=f.guid)),g?I.splice(I.delegateCount++,0,M):I.push(M),h.event.global[F]=!0)},remove:function(a,l,f,d,g){var v,b,A,x,P,M,V,I,F,he,xe,pe=L.hasData(a)&&L.get(a);if(!(!pe||!(x=pe.events))){for(l=(l||"").match(pt)||[""],P=l.length;P--;){if(A=Vl.exec(l[P])||[],F=xe=A[1],he=(A[2]||"").split(".").sort(),!F){for(F in x)h.event.remove(a,F+l[P],f,d,!0);continue}for(V=h.event.special[F]||{},F=(d?V.delegateType:V.bindType)||F,I=x[F]||[],A=A[2]&&new RegExp("(^|\\.)"+he.join("\\.(?:.*\\.|)")+"(\\.|$)"),b=v=I.length;v--;)M=I[v],(g||xe===M.origType)&&(!f||f.guid===M.guid)&&(!A||A.test(M.namespace))&&(!d||d===M.selector||d==="**"&&M.selector)&&(I.splice(v,1),M.selector&&I.delegateCount--,V.remove&&V.remove.call(a,M));b&&!I.length&&((!V.teardown||V.teardown.call(a,he,pe.handle)===!1)&&h.removeEvent(a,F,pe.handle),delete x[F])}h.isEmptyObject(x)&&L.remove(a,"handle events")}},dispatch:function(a){var l,f,d,g,v,b,A=new Array(arguments.length),x=h.event.fix(a),P=(L.get(this,"events")||Object.create(null))[x.type]||[],M=h.event.special[x.type]||{};for(A[0]=x,l=1;l=1)){for(;P!==this;P=P.parentNode||this)if(P.nodeType===1&&!(a.type==="click"&&P.disabled===!0)){for(v=[],b={},f=0;f-1:h.find(g,this,null,[P]).length),b[g]&&v.push(d);v.length&&A.push({elem:P,handlers:v})}}return P=this,x\s*$/g;function Yl(a,l){return Z(a,"table")&&Z(l.nodeType!==11?l:l.firstChild,"tr")&&h(a).children("tbody")[0]||a}function Uh(a){return a.type=(a.getAttribute("type")!==null)+"/"+a.type,a}function Wh(a){return(a.type||"").slice(0,5)==="true/"?a.type=a.type.slice(5):a.removeAttribute("type"),a}function Kl(a,l){var f,d,g,v,b,A,x;if(l.nodeType===1){if(L.hasData(a)&&(v=L.get(a),x=v.events,x)){L.remove(l,"handle events");for(g in x)for(f=0,d=x[g].length;f1&&typeof F=="string"&&!N.checkClone&&Bh.test(F))return a.each(function(xe){var pe=a.eq(xe);he&&(l[0]=F.call(this,xe,pe.html())),Hi(pe,l,f,d)});if(V&&(g=Wl(l,a[0].ownerDocument,!1,a,d),v=g.firstChild,g.childNodes.length===1&&(g=v),v||d)){for(b=h.map(At(g,"script"),Uh),A=b.length;M0&&xa(b,!x&&At(a,"script")),A},cleanData:function(a){for(var l,f,d,g=h.event.special,v=0;(f=a[v])!==void 0;v++)if(H(f)){if(l=f[L.expando]){if(l.events)for(d in l.events)g[d]?h.event.remove(f,d):h.removeEvent(f,d,l.handle);f[L.expando]=void 0}f[Q.expando]&&(f[Q.expando]=void 0)}}}),h.fn.extend({detach:function(a){return Ql(this,a,!0)},remove:function(a){return Ql(this,a)},text:function(a){return $e(this,function(l){return l===void 0?h.text(this):this.empty().each(function(){(this.nodeType===1||this.nodeType===11||this.nodeType===9)&&(this.textContent=l)})},null,a,arguments.length)},append:function(){return Hi(this,arguments,function(a){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var l=Yl(this,a);l.appendChild(a)}})},prepend:function(){return Hi(this,arguments,function(a){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var l=Yl(this,a);l.insertBefore(a,l.firstChild)}})},before:function(){return Hi(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Hi(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,l=0;(a=this[l])!=null;l++)a.nodeType===1&&(h.cleanData(At(a,!1)),a.textContent="");return this},clone:function(a,l){return a=a==null?!1:a,l=l==null?a:l,this.map(function(){return h.clone(this,a,l)})},html:function(a){return $e(this,function(l){var f=this[0]||{},d=0,g=this.length;if(l===void 0&&f.nodeType===1)return f.innerHTML;if(typeof l=="string"&&!qh.test(l)&&!ct[(cr.exec(l)||["",""])[1].toLowerCase()]){l=h.htmlPrefilter(l);try{for(;d=0&&(x+=Math.max(0,Math.ceil(a["offset"+l[0].toUpperCase()+l.slice(1)]-v-x-A-.5))||0),x}function ru(a,l,f){var d=is(a),g=!N.boxSizingReliable()||f,v=g&&h.css(a,"boxSizing",!1,d)==="border-box",b=v,A=fr(a,l,d),x="offset"+l[0].toUpperCase()+l.slice(1);if(Ta.test(A)){if(!f)return A;A="auto"}return(!N.boxSizingReliable()&&v||!N.reliableTrDimensions()&&Z(a,"tr")||A==="auto"||!parseFloat(A)&&h.css(a,"display",!1,d)==="inline")&&a.getClientRects().length&&(v=h.css(a,"boxSizing",!1,d)==="border-box",b=x in a,b&&(A=a[x])),A=parseFloat(A)||0,A+Sa(a,l,f||(v?"border":"content"),b,d,A)+"px"}h.extend({cssHooks:{opacity:{get:function(a,l){if(l){var f=fr(a,"opacity");return f===""?"1":f}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(a,l,f,d){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var g,v,b,A=E(l),x=Aa.test(l),P=a.style;if(x||(l=Ea(A)),b=h.cssHooks[l]||h.cssHooks[A],f!==void 0){if(v=typeof f,v==="string"&&(g=ue.exec(f))&&g[1]&&(f=ut(a,l,g),v="number"),f==null||f!==f)return;v==="number"&&!x&&(f+=g&&g[3]||(h.cssNumber[A]?"":"px")),!N.clearCloneStyle&&f===""&&l.indexOf("background")===0&&(P[l]="inherit"),(!b||!("set"in b)||(f=b.set(a,f,d))!==void 0)&&(x?P.setProperty(l,f):P[l]=f)}else return b&&"get"in b&&(g=b.get(a,!1,d))!==void 0?g:P[l]}},css:function(a,l,f,d){var g,v,b,A=E(l),x=Aa.test(l);return x||(l=Ea(A)),b=h.cssHooks[l]||h.cssHooks[A],b&&"get"in b&&(g=b.get(a,!0,f)),g===void 0&&(g=fr(a,l,d)),g==="normal"&&l in nu&&(g=nu[l]),f===""||f?(v=parseFloat(g),f===!0||isFinite(v)?v||0:g):g}}),h.each(["height","width"],function(a,l){h.cssHooks[l]={get:function(f,d,g){if(d)return Gh.test(h.css(f,"display"))&&(!f.getClientRects().length||!f.getBoundingClientRect().width)?Gl(f,Xh,function(){return ru(f,l,g)}):ru(f,l,g)},set:function(f,d,g){var v,b=is(f),A=!N.scrollboxSize()&&b.position==="absolute",x=A||g,P=x&&h.css(f,"boxSizing",!1,b)==="border-box",M=g?Sa(f,l,g,P,b):0;return P&&A&&(M-=Math.ceil(f["offset"+l[0].toUpperCase()+l.slice(1)]-parseFloat(b[l])-Sa(f,l,"border",!1,b)-.5)),M&&(v=ue.exec(d))&&(v[3]||"px")!=="px"&&(f.style[l]=d,d=h.css(f,l)),iu(f,d,M)}}}),h.cssHooks.marginLeft=Jl(N.reliableMarginLeft,function(a,l){if(l)return(parseFloat(fr(a,"marginLeft"))||a.getBoundingClientRect().left-Gl(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),h.each({margin:"",padding:"",border:"Width"},function(a,l){h.cssHooks[a+l]={expand:function(f){for(var d=0,g={},v=typeof f=="string"?f.split(" "):[f];d<4;d++)g[a+ce[d]+l]=v[d]||v[d-2]||v[0];return g}},a!=="margin"&&(h.cssHooks[a+l].set=iu)}),h.fn.extend({css:function(a,l){return $e(this,function(f,d,g){var v,b,A={},x=0;if(Array.isArray(d)){for(v=is(f),b=d.length;x1)}});function Ft(a,l,f,d,g){return new Ft.prototype.init(a,l,f,d,g)}h.Tween=Ft,Ft.prototype={constructor:Ft,init:function(a,l,f,d,g,v){this.elem=a,this.prop=f,this.easing=g||h.easing._default,this.options=l,this.start=this.now=this.cur(),this.end=d,this.unit=v||(h.cssNumber[f]?"":"px")},cur:function(){var a=Ft.propHooks[this.prop];return a&&a.get?a.get(this):Ft.propHooks._default.get(this)},run:function(a){var l,f=Ft.propHooks[this.prop];return this.options.duration?this.pos=l=h.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=l=a,this.now=(this.end-this.start)*l+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),f&&f.set?f.set(this):Ft.propHooks._default.set(this),this}},Ft.prototype.init.prototype=Ft.prototype,Ft.propHooks={_default:{get:function(a){var l;return a.elem.nodeType!==1||a.elem[a.prop]!=null&&a.elem.style[a.prop]==null?a.elem[a.prop]:(l=h.css(a.elem,a.prop,""),!l||l==="auto"?0:l)},set:function(a){h.fx.step[a.prop]?h.fx.step[a.prop](a):a.elem.nodeType===1&&(h.cssHooks[a.prop]||a.elem.style[Ea(a.prop)]!=null)?h.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Ft.propHooks.scrollTop=Ft.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},h.easing={linear:function(a){return a},swing:function(a){return .5-Math.cos(a*Math.PI)/2},_default:"swing"},h.fx=Ft.prototype.init,h.fx.step={};var Fi,rs,Jh=/^(?:toggle|show|hide)$/,Zh=/queueHooks$/;function Oa(){rs&&(R.hidden===!1&&t.requestAnimationFrame?t.requestAnimationFrame(Oa):t.setTimeout(Oa,h.fx.interval),h.fx.tick())}function su(){return t.setTimeout(function(){Fi=void 0}),Fi=Date.now()}function ss(a,l){var f,d=0,g={height:a};for(l=l?1:0;d<4;d+=2-l)f=ce[d],g["margin"+f]=g["padding"+f]=a;return l&&(g.opacity=g.width=a),g}function au(a,l,f){for(var d,g=(ln.tweeners[l]||[]).concat(ln.tweeners["*"]),v=0,b=g.length;v1)},removeAttr:function(a){return this.each(function(){h.removeAttr(this,a)})}}),h.extend({attr:function(a,l,f){var d,g,v=a.nodeType;if(!(v===3||v===8||v===2)){if(typeof a.getAttribute>"u")return h.prop(a,l,f);if((v!==1||!h.isXMLDoc(a))&&(g=h.attrHooks[l.toLowerCase()]||(h.expr.match.bool.test(l)?ou:void 0)),f!==void 0){if(f===null){h.removeAttr(a,l);return}return g&&"set"in g&&(d=g.set(a,f,l))!==void 0?d:(a.setAttribute(l,f+""),f)}return g&&"get"in g&&(d=g.get(a,l))!==null?d:(d=h.find.attr(a,l),d==null?void 0:d)}},attrHooks:{type:{set:function(a,l){if(!N.radioValue&&l==="radio"&&Z(a,"input")){var f=a.value;return a.setAttribute("type",l),f&&(a.value=f),l}}}},removeAttr:function(a,l){var f,d=0,g=l&&l.match(pt);if(g&&a.nodeType===1)for(;f=g[d++];)a.removeAttribute(f)}}),ou={set:function(a,l,f){return l===!1?h.removeAttr(a,f):a.setAttribute(f,f),f}},h.each(h.expr.match.bool.source.match(/\w+/g),function(a,l){var f=dr[l]||h.find.attr;dr[l]=function(d,g,v){var b,A,x=g.toLowerCase();return v||(A=dr[x],dr[x]=b,b=f(d,g,v)!=null?x:null,dr[x]=A),b}});var np=/^(?:input|select|textarea|button)$/i,ip=/^(?:a|area)$/i;h.fn.extend({prop:function(a,l){return $e(this,h.prop,a,l,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[h.propFix[a]||a]})}}),h.extend({prop:function(a,l,f){var d,g,v=a.nodeType;if(!(v===3||v===8||v===2))return(v!==1||!h.isXMLDoc(a))&&(l=h.propFix[l]||l,g=h.propHooks[l]),f!==void 0?g&&"set"in g&&(d=g.set(a,f,l))!==void 0?d:a[l]=f:g&&"get"in g&&(d=g.get(a,l))!==null?d:a[l]},propHooks:{tabIndex:{get:function(a){var l=h.find.attr(a,"tabindex");return l?parseInt(l,10):np.test(a.nodeName)||ip.test(a.nodeName)&&a.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),N.optSelected||(h.propHooks.selected={get:function(a){var l=a.parentNode;return l&&l.parentNode&&l.parentNode.selectedIndex,null},set:function(a){var l=a.parentNode;l&&(l.selectedIndex,l.parentNode&&l.parentNode.selectedIndex)}}),h.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){h.propFix[this.toLowerCase()]=this});function pi(a){var l=a.match(pt)||[];return l.join(" ")}function mi(a){return a.getAttribute&&a.getAttribute("class")||""}function Ra(a){return Array.isArray(a)?a:typeof a=="string"?a.match(pt)||[]:[]}h.fn.extend({addClass:function(a){var l,f,d,g,v,b;return D(a)?this.each(function(A){h(this).addClass(a.call(this,A,mi(this)))}):(l=Ra(a),l.length?this.each(function(){if(d=mi(this),f=this.nodeType===1&&" "+pi(d)+" ",f){for(v=0;v-1;)f=f.replace(" "+g+" "," ");b=pi(f),d!==b&&this.setAttribute("class",b)}}):this):this.attr("class","")},toggleClass:function(a,l){var f,d,g,v,b=typeof a,A=b==="string"||Array.isArray(a);return D(a)?this.each(function(x){h(this).toggleClass(a.call(this,x,mi(this),l),l)}):typeof l=="boolean"&&A?l?this.addClass(a):this.removeClass(a):(f=Ra(a),this.each(function(){if(A)for(v=h(this),g=0;g-1)return!0;return!1}});var rp=/\r/g;h.fn.extend({val:function(a){var l,f,d,g=this[0];return arguments.length?(d=D(a),this.each(function(v){var b;this.nodeType===1&&(d?b=a.call(this,v,h(this).val()):b=a,b==null?b="":typeof b=="number"?b+="":Array.isArray(b)&&(b=h.map(b,function(A){return A==null?"":A+""})),l=h.valHooks[this.type]||h.valHooks[this.nodeName.toLowerCase()],(!l||!("set"in l)||l.set(this,b,"value")===void 0)&&(this.value=b))})):g?(l=h.valHooks[g.type]||h.valHooks[g.nodeName.toLowerCase()],l&&"get"in l&&(f=l.get(g,"value"))!==void 0?f:(f=g.value,typeof f=="string"?f.replace(rp,""):f==null?"":f)):void 0}}),h.extend({valHooks:{option:{get:function(a){var l=h.find.attr(a,"value");return l!=null?l:pi(h.text(a))}},select:{get:function(a){var l,f,d,g=a.options,v=a.selectedIndex,b=a.type==="select-one",A=b?null:[],x=b?v+1:g.length;for(v<0?d=x:d=b?v:0;d-1)&&(f=!0);return f||(a.selectedIndex=-1),v}}}}),h.each(["radio","checkbox"],function(){h.valHooks[this]={set:function(a,l){if(Array.isArray(l))return a.checked=h.inArray(h(a).val(),l)>-1}},N.checkOn||(h.valHooks[this].get=function(a){return a.getAttribute("value")===null?"on":a.value})}),N.focusin="onfocusin"in t;var lu=/^(?:focusinfocus|focusoutblur)$/,uu=function(a){a.stopPropagation()};h.extend(h.event,{trigger:function(a,l,f,d){var g,v,b,A,x,P,M,V,I=[f||R],F=w.call(a,"type")?a.type:a,he=w.call(a,"namespace")?a.namespace.split("."):[];if(v=V=b=f=f||R,!(f.nodeType===3||f.nodeType===8)&&!lu.test(F+h.event.triggered)&&(F.indexOf(".")>-1&&(he=F.split("."),F=he.shift(),he.sort()),x=F.indexOf(":")<0&&"on"+F,a=a[h.expando]?a:new h.Event(F,typeof a=="object"&&a),a.isTrigger=d?2:3,a.namespace=he.join("."),a.rnamespace=a.namespace?new RegExp("(^|\\.)"+he.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,a.result=void 0,a.target||(a.target=f),l=l==null?[a]:h.makeArray(l,[a]),M=h.event.special[F]||{},!(!d&&M.trigger&&M.trigger.apply(f,l)===!1))){if(!d&&!M.noBubble&&!ee(f)){for(A=M.delegateType||F,lu.test(A+F)||(v=v.parentNode);v;v=v.parentNode)I.push(v),b=v;b===(f.ownerDocument||R)&&I.push(b.defaultView||b.parentWindow||t)}for(g=0;(v=I[g++])&&!a.isPropagationStopped();)V=v,a.type=g>1?A:M.bindType||F,P=(L.get(v,"events")||Object.create(null))[a.type]&&L.get(v,"handle"),P&&P.apply(v,l),P=x&&v[x],P&&P.apply&&H(v)&&(a.result=P.apply(v,l),a.result===!1&&a.preventDefault());return a.type=F,!d&&!a.isDefaultPrevented()&&(!M._default||M._default.apply(I.pop(),l)===!1)&&H(f)&&x&&D(f[F])&&!ee(f)&&(b=f[x],b&&(f[x]=null),h.event.triggered=F,a.isPropagationStopped()&&V.addEventListener(F,uu),f[F](),a.isPropagationStopped()&&V.removeEventListener(F,uu),h.event.triggered=void 0,b&&(f[x]=b)),a.result}},simulate:function(a,l,f){var d=h.extend(new h.Event,f,{type:a,isSimulated:!0});h.event.trigger(d,null,l)}}),h.fn.extend({trigger:function(a,l){return this.each(function(){h.event.trigger(a,l,this)})},triggerHandler:function(a,l){var f=this[0];if(f)return h.event.trigger(a,l,f,!0)}}),N.focusin||h.each({focus:"focusin",blur:"focusout"},function(a,l){var f=function(d){h.event.simulate(l,d.target,h.event.fix(d))};h.event.special[l]={setup:function(){var d=this.ownerDocument||this.document||this,g=L.access(d,l);g||d.addEventListener(a,f,!0),L.access(d,l,(g||0)+1)},teardown:function(){var d=this.ownerDocument||this.document||this,g=L.access(d,l)-1;g?L.access(d,l,g):(d.removeEventListener(a,f,!0),L.remove(d,l))}}});var hr=t.location,cu={guid:Date.now()},za=/\?/;h.parseXML=function(a){var l,f;if(!a||typeof a!="string")return null;try{l=new t.DOMParser().parseFromString(a,"text/xml")}catch{}return f=l&&l.getElementsByTagName("parsererror")[0],(!l||f)&&h.error("Invalid XML: "+(f?h.map(f.childNodes,function(d){return d.textContent}).join(` +`):a)),l};var sp=/\[\]$/,fu=/\r?\n/g,ap=/^(?:submit|button|image|reset|file)$/i,op=/^(?:input|select|textarea|keygen)/i;function Pa(a,l,f,d){var g;if(Array.isArray(l))h.each(l,function(v,b){f||sp.test(a)?d(a,b):Pa(a+"["+(typeof b=="object"&&b!=null?v:"")+"]",b,f,d)});else if(!f&&ae(l)==="object")for(g in l)Pa(a+"["+g+"]",l[g],f,d);else d(a,l)}h.param=function(a,l){var f,d=[],g=function(v,b){var A=D(b)?b():b;d[d.length]=encodeURIComponent(v)+"="+encodeURIComponent(A==null?"":A)};if(a==null)return"";if(Array.isArray(a)||a.jquery&&!h.isPlainObject(a))h.each(a,function(){g(this.name,this.value)});else for(f in a)Pa(f,a[f],l,g);return d.join("&")},h.fn.extend({serialize:function(){return h.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=h.prop(this,"elements");return a?h.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!h(this).is(":disabled")&&op.test(this.nodeName)&&!ap.test(a)&&(this.checked||!on.test(a))}).map(function(a,l){var f=h(this).val();return f==null?null:Array.isArray(f)?h.map(f,function(d){return{name:l.name,value:d.replace(fu,`\r +`)}}):{name:l.name,value:f.replace(fu,`\r +`)}}).get()}});var lp=/%20/g,up=/#.*$/,cp=/([?&])_=[^&]*/,fp=/^(.*?):[ \t]*([^\r\n]*)$/mg,dp=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,hp=/^(?:GET|HEAD)$/,pp=/^\/\//,du={},La={},hu="*/".concat("*"),Ia=R.createElement("a");Ia.href=hr.href;function pu(a){return function(l,f){typeof l!="string"&&(f=l,l="*");var d,g=0,v=l.toLowerCase().match(pt)||[];if(D(f))for(;d=v[g++];)d[0]==="+"?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(f)):(a[d]=a[d]||[]).push(f)}}function mu(a,l,f,d){var g={},v=a===La;function b(A){var x;return g[A]=!0,h.each(a[A]||[],function(P,M){var V=M(l,f,d);if(typeof V=="string"&&!v&&!g[V])return l.dataTypes.unshift(V),b(V),!1;if(v)return!(x=V)}),x}return b(l.dataTypes[0])||!g["*"]&&b("*")}function Na(a,l){var f,d,g=h.ajaxSettings.flatOptions||{};for(f in l)l[f]!==void 0&&((g[f]?a:d||(d={}))[f]=l[f]);return d&&h.extend(!0,a,d),a}function mp(a,l,f){for(var d,g,v,b,A=a.contents,x=a.dataTypes;x[0]==="*";)x.shift(),d===void 0&&(d=a.mimeType||l.getResponseHeader("Content-Type"));if(d){for(g in A)if(A[g]&&A[g].test(d)){x.unshift(g);break}}if(x[0]in f)v=x[0];else{for(g in f){if(!x[0]||a.converters[g+" "+x[0]]){v=g;break}b||(b=g)}v=v||b}if(v)return v!==x[0]&&x.unshift(v),f[v]}function gp(a,l,f,d){var g,v,b,A,x,P={},M=a.dataTypes.slice();if(M[1])for(b in a.converters)P[b.toLowerCase()]=a.converters[b];for(v=M.shift();v;)if(a.responseFields[v]&&(f[a.responseFields[v]]=l),!x&&d&&a.dataFilter&&(l=a.dataFilter(l,a.dataType)),x=v,v=M.shift(),v){if(v==="*")v=x;else if(x!=="*"&&x!==v){if(b=P[x+" "+v]||P["* "+v],!b){for(g in P)if(A=g.split(" "),A[1]===v&&(b=P[x+" "+A[0]]||P["* "+A[0]],b)){b===!0?b=P[g]:P[g]!==!0&&(v=A[0],M.unshift(A[1]));break}}if(b!==!0)if(b&&a.throws)l=b(l);else try{l=b(l)}catch(V){return{state:"parsererror",error:b?V:"No conversion from "+x+" to "+v}}}}return{state:"success",data:l}}h.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:hr.href,type:"GET",isLocal:dp.test(hr.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":hu,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":h.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,l){return l?Na(Na(a,h.ajaxSettings),l):Na(h.ajaxSettings,a)},ajaxPrefilter:pu(du),ajaxTransport:pu(La),ajax:function(a,l){typeof a=="object"&&(l=a,a=void 0),l=l||{};var f,d,g,v,b,A,x,P,M,V,I=h.ajaxSetup({},l),F=I.context||I,he=I.context&&(F.nodeType||F.jquery)?h(F):h.event,xe=h.Deferred(),pe=h.Callbacks("once memory"),kt=I.statusCode||{},bt={},Yt={},Ke="canceled",Ce={readyState:0,getResponseHeader:function(Ne){var ft;if(x){if(!v)for(v={};ft=fp.exec(g);)v[ft[1].toLowerCase()+" "]=(v[ft[1].toLowerCase()+" "]||[]).concat(ft[2]);ft=v[Ne.toLowerCase()+" "]}return ft==null?null:ft.join(", ")},getAllResponseHeaders:function(){return x?g:null},setRequestHeader:function(Ne,ft){return x==null&&(Ne=Yt[Ne.toLowerCase()]=Yt[Ne.toLowerCase()]||Ne,bt[Ne]=ft),this},overrideMimeType:function(Ne){return x==null&&(I.mimeType=Ne),this},statusCode:function(Ne){var ft;if(Ne)if(x)Ce.always(Ne[Ce.status]);else for(ft in Ne)kt[ft]=[kt[ft],Ne[ft]];return this},abort:function(Ne){var ft=Ne||Ke;return f&&f.abort(ft),qt(0,ft),this}};if(xe.promise(Ce),I.url=((a||I.url||hr.href)+"").replace(pp,hr.protocol+"//"),I.type=l.method||l.type||I.method||I.type,I.dataTypes=(I.dataType||"*").toLowerCase().match(pt)||[""],I.crossDomain==null){A=R.createElement("a");try{A.href=I.url,A.href=A.href,I.crossDomain=Ia.protocol+"//"+Ia.host!=A.protocol+"//"+A.host}catch{I.crossDomain=!0}}if(I.data&&I.processData&&typeof I.data!="string"&&(I.data=h.param(I.data,I.traditional)),mu(du,I,l,Ce),x)return Ce;P=h.event&&I.global,P&&h.active++===0&&h.event.trigger("ajaxStart"),I.type=I.type.toUpperCase(),I.hasContent=!hp.test(I.type),d=I.url.replace(up,""),I.hasContent?I.data&&I.processData&&(I.contentType||"").indexOf("application/x-www-form-urlencoded")===0&&(I.data=I.data.replace(lp,"+")):(V=I.url.slice(d.length),I.data&&(I.processData||typeof I.data=="string")&&(d+=(za.test(d)?"&":"?")+I.data,delete I.data),I.cache===!1&&(d=d.replace(cp,"$1"),V=(za.test(d)?"&":"?")+"_="+cu.guid+++V),I.url=d+V),I.ifModified&&(h.lastModified[d]&&Ce.setRequestHeader("If-Modified-Since",h.lastModified[d]),h.etag[d]&&Ce.setRequestHeader("If-None-Match",h.etag[d])),(I.data&&I.hasContent&&I.contentType!==!1||l.contentType)&&Ce.setRequestHeader("Content-Type",I.contentType),Ce.setRequestHeader("Accept",I.dataTypes[0]&&I.accepts[I.dataTypes[0]]?I.accepts[I.dataTypes[0]]+(I.dataTypes[0]!=="*"?", "+hu+"; q=0.01":""):I.accepts["*"]);for(M in I.headers)Ce.setRequestHeader(M,I.headers[M]);if(I.beforeSend&&(I.beforeSend.call(F,Ce,I)===!1||x))return Ce.abort();if(Ke="abort",pe.add(I.complete),Ce.done(I.success),Ce.fail(I.error),f=mu(La,I,l,Ce),!f)qt(-1,"No Transport");else{if(Ce.readyState=1,P&&he.trigger("ajaxSend",[Ce,I]),x)return Ce;I.async&&I.timeout>0&&(b=t.setTimeout(function(){Ce.abort("timeout")},I.timeout));try{x=!1,f.send(bt,qt)}catch(Ne){if(x)throw Ne;qt(-1,Ne)}}function qt(Ne,ft,mr,as){var Kt,gi,vi,Bt,Vn,tn=ft;x||(x=!0,b&&t.clearTimeout(b),f=void 0,g=as||"",Ce.readyState=Ne>0?4:0,Kt=Ne>=200&&Ne<300||Ne===304,mr&&(Bt=mp(I,Ce,mr)),!Kt&&h.inArray("script",I.dataTypes)>-1&&h.inArray("json",I.dataTypes)<0&&(I.converters["text script"]=function(){}),Bt=gp(I,Bt,Ce,Kt),Kt?(I.ifModified&&(Vn=Ce.getResponseHeader("Last-Modified"),Vn&&(h.lastModified[d]=Vn),Vn=Ce.getResponseHeader("etag"),Vn&&(h.etag[d]=Vn)),Ne===204||I.type==="HEAD"?tn="nocontent":Ne===304?tn="notmodified":(tn=Bt.state,gi=Bt.data,vi=Bt.error,Kt=!vi)):(vi=tn,(Ne||!tn)&&(tn="error",Ne<0&&(Ne=0))),Ce.status=Ne,Ce.statusText=(ft||tn)+"",Kt?xe.resolveWith(F,[gi,tn,Ce]):xe.rejectWith(F,[Ce,tn,vi]),Ce.statusCode(kt),kt=void 0,P&&he.trigger(Kt?"ajaxSuccess":"ajaxError",[Ce,I,Kt?gi:vi]),pe.fireWith(F,[Ce,tn]),P&&(he.trigger("ajaxComplete",[Ce,I]),--h.active||h.event.trigger("ajaxStop")))}return Ce},getJSON:function(a,l,f){return h.get(a,l,f,"json")},getScript:function(a,l){return h.get(a,void 0,l,"script")}}),h.each(["get","post"],function(a,l){h[l]=function(f,d,g,v){return D(d)&&(v=v||g,g=d,d=void 0),h.ajax(h.extend({url:f,type:l,dataType:v,data:d,success:g},h.isPlainObject(f)&&f))}}),h.ajaxPrefilter(function(a){var l;for(l in a.headers)l.toLowerCase()==="content-type"&&(a.contentType=a.headers[l]||"")}),h._evalUrl=function(a,l,f){return h.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(d){h.globalEval(d,l,f)}})},h.fn.extend({wrapAll:function(a){var l;return this[0]&&(D(a)&&(a=a.call(this[0])),l=h(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&l.insertBefore(this[0]),l.map(function(){for(var f=this;f.firstElementChild;)f=f.firstElementChild;return f}).append(this)),this},wrapInner:function(a){return D(a)?this.each(function(l){h(this).wrapInner(a.call(this,l))}):this.each(function(){var l=h(this),f=l.contents();f.length?f.wrapAll(a):l.append(a)})},wrap:function(a){var l=D(a);return this.each(function(f){h(this).wrapAll(l?a.call(this,f):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){h(this).replaceWith(this.childNodes)}),this}}),h.expr.pseudos.hidden=function(a){return!h.expr.pseudos.visible(a)},h.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},h.ajaxSettings.xhr=function(){try{return new t.XMLHttpRequest}catch{}};var vp={0:200,1223:204},pr=h.ajaxSettings.xhr();N.cors=!!pr&&"withCredentials"in pr,N.ajax=pr=!!pr,h.ajaxTransport(function(a){var l,f;if(N.cors||pr&&!a.crossDomain)return{send:function(d,g){var v,b=a.xhr();if(b.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(v in a.xhrFields)b[v]=a.xhrFields[v];a.mimeType&&b.overrideMimeType&&b.overrideMimeType(a.mimeType),!a.crossDomain&&!d["X-Requested-With"]&&(d["X-Requested-With"]="XMLHttpRequest");for(v in d)b.setRequestHeader(v,d[v]);l=function(A){return function(){l&&(l=f=b.onload=b.onerror=b.onabort=b.ontimeout=b.onreadystatechange=null,A==="abort"?b.abort():A==="error"?typeof b.status!="number"?g(0,"error"):g(b.status,b.statusText):g(vp[b.status]||b.status,b.statusText,(b.responseType||"text")!=="text"||typeof b.responseText!="string"?{binary:b.response}:{text:b.responseText},b.getAllResponseHeaders()))}},b.onload=l(),f=b.onerror=b.ontimeout=l("error"),b.onabort!==void 0?b.onabort=f:b.onreadystatechange=function(){b.readyState===4&&t.setTimeout(function(){l&&f()})},l=l("abort");try{b.send(a.hasContent&&a.data||null)}catch(A){if(l)throw A}},abort:function(){l&&l()}}}),h.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),h.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return h.globalEval(a),a}}}),h.ajaxPrefilter("script",function(a){a.cache===void 0&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),h.ajaxTransport("script",function(a){if(a.crossDomain||a.scriptAttrs){var l,f;return{send:function(d,g){l=h(" - + +
-- 2.45.3 From 785354032e94905b58b1d0a3726a040fd394d524 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Sun, 12 Nov 2023 10:12:17 +0100 Subject: [PATCH 09/18] GUI improvements, can now start client and web browser --- pyproject.toml | 3 +- syng/gui.py | 125 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eff6d42..2cf4967 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ syng-server = "syng.server:main" syng-shell = "syng.webclientmockup:main" [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8" pytube = "*" aiohttp = "^3.8.3" python-socketio = "^5.7.2" @@ -26,6 +26,7 @@ customtkinter = "^5.2.1" qrcode = "^7.4.2" pymediainfo = "^6.1.0" pyyaml = "^6.0.1" +async-tkinter-loop = "^0.9.2" [build-system] requires = ["poetry-core"] diff --git a/syng/gui.py b/syng/gui.py index 5712662..dda9ebf 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,20 +1,25 @@ +import asyncio import builtins +from functools import partial +import webbrowser from yaml import load, Loader import customtkinter import qrcode import secrets import string from tkinter import filedialog +from async_tkinter_loop import async_handler, async_mainloop +from async_tkinter_loop.mixins import AsyncCTk -from syng.client import default_config +from .client import default_config, start_client from .sources import available_sources -class OptionFrame(customtkinter.CTkFrame): +class OptionFrame(customtkinter.CTkScrollableFrame): def add_option_label(self, text): customtkinter.CTkLabel(self, text=text, justify="left").grid( - column=0, row=self.number_of_options, padx=5, pady=5 + column=0, row=self.number_of_options, padx=5, pady=5, sticky="ne" ) def add_bool_option(self, name, description, value=False): @@ -29,7 +34,7 @@ class OptionFrame(customtkinter.CTkFrame): self.bool_options[name].select() else: self.bool_options[name].deselect() - self.bool_options[name].grid(column=1, row=self.number_of_options) + self.bool_options[name].grid(column=1, row=self.number_of_options, sticky="EW") self.number_of_options += 1 def add_string_option(self, name, description, value="", callback=None): @@ -40,33 +45,68 @@ class OptionFrame(customtkinter.CTkFrame): self.string_options[name] = customtkinter.CTkTextbox( self, wrap="none", height=1 ) - self.string_options[name].grid(column=1, row=self.number_of_options) + self.string_options[name].grid( + column=1, row=self.number_of_options, sticky="EW" + ) self.string_options[name].insert("0.0", value) if callback is not None: self.string_options[name].bind("", callback) self.string_options[name].bind("", callback) self.number_of_options += 1 + def del_list_element(self, name, element, frame): + self.list_options[name].remove(element) + frame.destroy() + + def add_list_element(self, name, frame, init, callback): + input_and_minus = customtkinter.CTkFrame(frame) + input_and_minus.pack(side="top", fill="x", expand=True) + input_field = customtkinter.CTkTextbox(input_and_minus, wrap="none", height=1) + input_field.pack(side="left", fill="x", expand=True) + input_field.insert("0.0", init) + if callback is not None: + input_field.bind("", callback) + input_field.bind("", callback) + + minus_button = customtkinter.CTkButton( + input_and_minus, + text="-", + width=40, + command=partial(self.del_list_element, name, input_field, input_and_minus), + ) + minus_button.pack(side="right") + self.list_options[name].append(input_field) + def add_list_option(self, name, description, value=[], callback=None): self.add_option_label(description) - self.list_options[name] = customtkinter.CTkTextbox(self, wrap="none", height=1) - self.list_options[name].grid(column=1, row=self.number_of_options) - self.list_options[name].insert("0.0", ", ".join(value)) - if callback is not None: - self.list_options[name].bind("", callback) - self.list_options[name].bind("", callback) + frame = customtkinter.CTkFrame(self) + frame.grid(column=1, row=self.number_of_options, sticky="EW") + + self.list_options[name] = [] + for v in value: + self.add_list_element(name, frame, v, callback) + plus_button = customtkinter.CTkButton( + frame, + text="+", + command=partial(self.add_list_element, name, frame, "", callback), + ) + plus_button.pack(side="bottom", fill="x", expand=True) + self.number_of_options += 1 def add_choose_option(self, name, description, values, value=""): self.add_option_label(description) self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) - self.choose_options[name].grid(column=1, row=self.number_of_options) + self.choose_options[name].grid( + column=1, row=self.number_of_options, sticky="EW" + ) self.choose_options[name].set(value) self.number_of_options += 1 def __init__(self, parent): super().__init__(parent) + self.columnconfigure((1,), weight=1) self.number_of_options = 0 self.string_options = {} self.choose_options = {} @@ -84,10 +124,10 @@ class OptionFrame(customtkinter.CTkFrame): for name, checkbox in self.bool_options.items(): config[name] = checkbox.get() == 1 - for name, textbox in self.list_options.items(): - config[name] = [ - v.strip() for v in textbox.get("0.0", "end").strip().split(",") - ] + for name, textboxes in self.list_options.items(): + config[name] = [] + for textbox in textboxes: + config[name].append(textbox.get("0.0", "end").strip()) return config @@ -109,9 +149,7 @@ class SourceTab(OptionFrame): super().__init__(parent) source = available_sources[source_name] self.vars: dict[str, str | bool | list[str]] = {} - for row, (name, (typ, desc, default)) in enumerate( - source.config_schema.items() - ): + for name, (typ, desc, default) in source.config_schema.items(): value = config[name] if name in config else default match typ: case builtins.bool: @@ -133,20 +171,15 @@ class GeneralConfig(OptionFrame): "waiting_room_policy", "Waiting room policy", ["forced", "optional", "none"], - config["waiting_room_policy"], + str(config["waiting_room_policy"]).lower(), + ) + self.add_string_option( + "last_song", "Time of last song\nin ISO-8601", config["last_song"] ) - self.add_string_option("last_song", "Time of last song", config["last_song"]) self.add_string_option( "preview_duration", "Preview Duration", config["preview_duration"] ) - for name, textbox in self.string_options.items(): - if config[name]: - textbox.insert("0.0", config[name]) - - for name, optionmenu in self.choose_options.items(): - optionmenu.set(str(config[name]).lower()) - def get_config(self): config = super().get_config() try: @@ -157,7 +190,7 @@ class GeneralConfig(OptionFrame): return config -class SyngGui(customtkinter.CTk): +class SyngGui(customtkinter.CTk, AsyncCTk): def loadConfig(self): filedialog.askopenfilename() @@ -177,6 +210,7 @@ class SyngGui(customtkinter.CTk): self.wm_title("Syng") + # Buttons fileframe = customtkinter.CTkFrame(self) fileframe.pack(side="bottom") @@ -192,10 +226,16 @@ class SyngGui(customtkinter.CTk): ) startbutton.pack(side="right") + open_web_button = customtkinter.CTkButton( + fileframe, text="Open Web", command=self.open_web + ) + open_web_button.pack(side="left") + + # Tabs and QR Code frm = customtkinter.CTkFrame(self) frm.pack(ipadx=10, padx=10, fill="both", expand=True) - tabview = customtkinter.CTkTabview(frm) + tabview = customtkinter.CTkTabview(frm, width=600, height=500) tabview.pack(side="right", padx=10, pady=10, fill="both", expand=True) tabview.add("General") @@ -204,12 +244,12 @@ class SyngGui(customtkinter.CTk): tabview.set("General") self.qrlabel = customtkinter.CTkLabel(frm, text="") - self.qrlabel.pack(side="left") + self.qrlabel.pack(side="left", anchor="n", padx=10, pady=10) self.general_config = GeneralConfig( tabview.tab("General"), config["config"], self.updateQr ) - self.general_config.pack(ipadx=10, fill="y") + self.general_config.pack(ipadx=10, fill="both", expand=True) self.tabs = {} @@ -222,11 +262,12 @@ class SyngGui(customtkinter.CTk): self.tabs[source_name] = SourceTab( tabview.tab(source_name), source_name, source_config ) - self.tabs[source_name].pack(ipadx=10) + self.tabs[source_name].pack(ipadx=10, expand=True, fill="both") self.updateQr() - def start(self): + @async_handler + async def start(self): sources = {} for source, tab in self.tabs.items(): sources[source] = tab.get_config() @@ -235,6 +276,14 @@ class SyngGui(customtkinter.CTk): config = {"sources": sources, "config": general_config} print(config) + await start_client(config) + + def open_web(self): + config = self.general_config.get_config() + server = config["server"] + server += "" if server.endswith("/") else "/" + room = config["room"] + webbrowser.open(server + room) def changeQr(self, data: str): qr = qrcode.QRCode(box_size=20, border=2) @@ -254,9 +303,11 @@ class SyngGui(customtkinter.CTk): self.changeQr(server + room) -def main(): - SyngGui().mainloop() +# async def main(): +# gui = SyngGui() +# await gui.run() if __name__ == "__main__": - main() + # asyncio.run(main()) + SyngGui().async_mainloop() -- 2.45.3 From f5a8b16a7f63d180cd8597c094977a4efc81d7b8 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Mon, 13 Nov 2023 19:11:20 +0100 Subject: [PATCH 10/18] gui can now launch client and server (almost there) --- pyproject.toml | 3 + syng/client.py | 7 +- syng/gui.py | 141 ++++++++++++++++++++++++++++++++------ syng/sources/__init__.py | 3 +- syng/sources/filebased.py | 2 +- syng/static/syng.png | Bin 0 -> 43591 bytes 6 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 syng/static/syng.png diff --git a/pyproject.toml b/pyproject.toml index 2cf4967..7b7f246 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ include = ["syng/static"] [tool.poetry.scripts] syng-client = "syng.client:main" syng-server = "syng.server:main" +syng-gui = "syng.gui:main" syng-shell = "syng.webclientmockup:main" [tool.poetry.dependencies] @@ -27,6 +28,8 @@ qrcode = "^7.4.2" pymediainfo = "^6.1.0" pyyaml = "^6.0.1" async-tkinter-loop = "^0.9.2" +tkcalendar = "^1.6.1" +tktimepicker = "^2.0.2" [build-system] requires = ["poetry-core"] diff --git a/syng/client.py b/syng/client.py index ea27b23..f5da742 100644 --- a/syng/client.py +++ b/syng/client.py @@ -65,6 +65,8 @@ currentLock: asyncio.Semaphore = asyncio.Semaphore(0) def default_config() -> dict[str, Optional[int | str]]: return { + "server": "http://localhost:8080", + "room": "ABCD", "preview_duration": 3, "secret": None, "last_song": None, @@ -416,6 +418,9 @@ async def aiomain() -> None: """ pass +def create_async_and_start_client(config): + asyncio.run(start_client(config)) + def main() -> None: """Entry point for the syng-client script.""" @@ -443,7 +448,7 @@ def main() -> None: if args.server: config["config"] |= {"server": args.server} - asyncio.run(start_client(config)) + create_async_and_start_client(config) if __name__ == "__main__": diff --git a/syng/gui.py b/syng/gui.py index dda9ebf..3803b87 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,19 +1,57 @@ import asyncio +from datetime import datetime, date, time +import os import builtins from functools import partial import webbrowser +import PIL from yaml import load, Loader +import multiprocessing import customtkinter import qrcode import secrets import string -from tkinter import filedialog -from async_tkinter_loop import async_handler, async_mainloop -from async_tkinter_loop.mixins import AsyncCTk +from tkinter import PhotoImage, filedialog +from tkcalendar import Calendar +from tktimepicker import SpinTimePickerOld, AnalogPicker, AnalogThemes +from tktimepicker import constants -from .client import default_config, start_client +from .client import create_async_and_start_client, default_config, start_client from .sources import available_sources +from .server import main as server_main + +class DateAndTimePickerWindow(customtkinter.CTkToplevel): + def __init__(self, parent, input_field): + super().__init__(parent) + self.calendar = Calendar(self) + self.calendar.pack(expand=True, fill="both") + self.timepicker = AnalogPicker(self, type=constants.HOURS12) + theme = AnalogThemes(self.timepicker) + theme.setDracula() + # self.timepicker.addAll(constants.HOURS24) + self.timepicker.pack(expand=True, fill="both") + + button = customtkinter.CTkButton(self, text="Ok", command=partial(self.insert, input_field)) + button.pack(expand=True, fill='x') + + def insert(self, input_field: customtkinter.CTkTextbox): + input_field.delete("0.0", "end") + selected_date = self.calendar.selection_get() + print(type(selected_date)) + if not isinstance(selected_date, date): + return + hours, minutes, ampm = self.timepicker.time() + if ampm == "PM": + hours = (hours + 12) % 24 + + selected_datetime = datetime.combine(selected_date, time(hours, minutes)) + input_field.insert("0.0", selected_datetime.isoformat()) + self.withdraw() + self.destroy() + + + class OptionFrame(customtkinter.CTkScrollableFrame): @@ -104,6 +142,35 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.choose_options[name].set(value) self.number_of_options += 1 + def open_date_and_time_picker(self, name, input_field): + if name not in self.date_and_time_pickers or not self.date_and_time_pickers[name].winfo_exists(): + self.date_and_time_pickers[name] = DateAndTimePickerWindow(self, input_field) + else: + self.date_and_time_pickers[name].focus() + + + def add_date_time_option(self, name, description, value): + self.add_option_label(description) + self.date_time_options[name] = None + input_and_button = customtkinter.CTkFrame(self) + input_and_button.grid(column=1, row=self.number_of_options, sticky="EW") + input_field = customtkinter.CTkTextbox(input_and_button, wrap="none", height=1) + input_field.pack(side="left", fill="x", expand=True) + try: + datetime.fromisoformat(value) + except TypeError: + value = "" + input_field.insert("0.0", value) + + button = customtkinter.CTkButton( + input_and_button, + text="...", + width=40, + command=partial(self.open_date_and_time_picker, name, input_field), + ) + button.pack(side="right") + self.number_of_options += 1 + def __init__(self, parent): super().__init__(parent) self.columnconfigure((1,), weight=1) @@ -112,6 +179,8 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.choose_options = {} self.bool_options = {} self.list_options = {} + self.date_time_options = {} + self.date_and_time_pickers = {} def get_config(self): config = {} @@ -173,9 +242,10 @@ class GeneralConfig(OptionFrame): ["forced", "optional", "none"], str(config["waiting_room_policy"]).lower(), ) - self.add_string_option( - "last_song", "Time of last song\nin ISO-8601", config["last_song"] - ) + # self.add_string_option( + # "last_song", "Time of last song\nin ISO-8601", config["last_song"] + # ) + self.add_date_time_option("last_song", "Time of last song", config["last_song"]) self.add_string_option( "preview_duration", "Preview Duration", config["preview_duration"] ) @@ -190,15 +260,37 @@ class GeneralConfig(OptionFrame): return config -class SyngGui(customtkinter.CTk, AsyncCTk): +class SyngGui(customtkinter.CTk): def loadConfig(self): filedialog.askopenfilename() + def on_close(self): + if self.server is not None: + self.server.kill() + + if self.client is not None: + self.client.kill() + + self.withdraw() + self.destroy() + def __init__(self): super().__init__(className="Syng") + self.protocol("WM_DELETE_WINDOW", self.on_close) - with open("syng-client.yaml") as cfile: - loaded_config = load(cfile, Loader=Loader) + rel_path = os.path.dirname(__file__) + img = PIL.ImageTk.PhotoImage(file=os.path.join(rel_path,"static/syng.png")) + self.wm_iconbitmap() + self.iconphoto(False, img) + + self.server = None + self.client = None + + try: + with open("syng-client.yaml") as cfile: + loaded_config = load(cfile, Loader=Loader) + except FileNotFoundError: + loaded_config = {} config = {"sources": {}, "config": default_config()} if "config" in loaded_config: config["config"] |= loaded_config["config"] @@ -222,10 +314,16 @@ class SyngGui(customtkinter.CTk, AsyncCTk): loadbutton.pack(side="left") startbutton = customtkinter.CTkButton( - fileframe, text="Start", command=self.start + fileframe, text="Start", command=self.start_client ) startbutton.pack(side="right") + startserverbutton = customtkinter.CTkButton( + fileframe, text="Start Server", command=self.start_server + ) + startserverbutton.pack(side="right") + + open_web_button = customtkinter.CTkButton( fileframe, text="Open Web", command=self.open_web ) @@ -266,8 +364,7 @@ class SyngGui(customtkinter.CTk, AsyncCTk): self.updateQr() - @async_handler - async def start(self): + def start_client(self): sources = {} for source, tab in self.tabs.items(): sources[source] = tab.get_config() @@ -275,8 +372,14 @@ class SyngGui(customtkinter.CTk, AsyncCTk): general_config = self.general_config.get_config() config = {"sources": sources, "config": general_config} - print(config) - await start_client(config) + # print(config) + self.client = multiprocessing.Process(target=create_async_and_start_client, args=(config,)) + self.client.start() + + def start_server(self): + self.server = multiprocessing.Process(target=server_main) + self.server.start() + def open_web(self): config = self.general_config.get_config() @@ -303,11 +406,9 @@ class SyngGui(customtkinter.CTk, AsyncCTk): self.changeQr(server + room) -# async def main(): -# gui = SyngGui() -# await gui.run() +def main(): + SyngGui().mainloop() if __name__ == "__main__": - # asyncio.run(main()) - SyngGui().async_mainloop() + main() diff --git a/syng/sources/__init__.py b/syng/sources/__init__.py index b57aefc..976615f 100644 --- a/syng/sources/__init__.py +++ b/syng/sources/__init__.py @@ -26,5 +26,6 @@ def configure_sources(configs: dict[str, Any]) -> dict[str, Source]: configured_sources = {} for source, config in configs.items(): if source in available_sources: - configured_sources[source] = available_sources[source](config) + if config["enabled"]: + configured_sources[source] = available_sources[source](config) return configured_sources diff --git a/syng/sources/filebased.py b/syng/sources/filebased.py index 64577e2..dc59be0 100644 --- a/syng/sources/filebased.py +++ b/syng/sources/filebased.py @@ -19,7 +19,7 @@ class FileBasedSource(Source): "extensions": ( list, "List of filename extensions\n(mp3+cdg, mp4, ...)", - "mp3+cdg", + ["mp3+cdg"], ) } diff --git a/syng/static/syng.png b/syng/static/syng.png new file mode 100644 index 0000000000000000000000000000000000000000..f60bac32ed97c20fc84a596e86e1260a83cfade6 GIT binary patch literal 43591 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 literal 0 HcmV?d00001 -- 2.45.3 From f7a21c6133c13ab1b77bd6add77cabe8cdd3836d Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Tue, 14 Nov 2023 17:03:29 +0100 Subject: [PATCH 11/18] Stopping the client --- syng/client.py | 22 ++++++---------- syng/gui.py | 54 ++++++++++++++++++++++++---------------- syng/sources/__init__.py | 2 +- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/syng/client.py b/syng/client.py index f5da742..af7a55f 100644 --- a/syng/client.py +++ b/syng/client.py @@ -36,6 +36,7 @@ import logging import secrets import string import tempfile +import signal from argparse import ArgumentParser from dataclasses import dataclass from dataclasses import field @@ -374,6 +375,10 @@ async def handle_request_config(data: dict[str, Any]) -> None: await sio.emit("config", {"source": data["source"], "config": config}) +def terminate(*args): + print("OOPS") + + async def start_client(config: dict[str, Any]) -> None: """ Initialize the client and connect to the server. @@ -382,11 +387,12 @@ async def start_client(config: dict[str, Any]) -> None: :type config: dict[str, Any] :rtype: None """ + sources.update(configure_sources(config["sources"])) if "config" in config: last_song = ( - datetime.datetime.fromisoformat(config["config"]["last_song"]) + datetime.datetime.fromisoformat(config["config"]["last_song"]).timestamp() if "last_song" in config["config"] and config["config"]["last_song"] else None ) @@ -403,21 +409,9 @@ async def start_client(config: dict[str, Any]) -> None: await sio.connect(state.config["server"]) await sio.wait() + print("exit") -async def aiomain() -> None: - """ - Async main function. - - Parses the arguments, reads a config file and sets default values. Then - connects to a specified server. - - If no secret is given, a random secret will be generated and presented to - the user. - - """ - pass - def create_async_and_start_client(config): asyncio.run(start_client(config)) diff --git a/syng/gui.py b/syng/gui.py index 3803b87..7a1cf19 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -21,6 +21,7 @@ from .client import create_async_and_start_client, default_config, start_client from .sources import available_sources from .server import main as server_main + class DateAndTimePickerWindow(customtkinter.CTkToplevel): def __init__(self, parent, input_field): super().__init__(parent) @@ -32,8 +33,10 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): # self.timepicker.addAll(constants.HOURS24) self.timepicker.pack(expand=True, fill="both") - button = customtkinter.CTkButton(self, text="Ok", command=partial(self.insert, input_field)) - button.pack(expand=True, fill='x') + button = customtkinter.CTkButton( + self, text="Ok", command=partial(self.insert, input_field) + ) + button.pack(expand=True, fill="x") def insert(self, input_field: customtkinter.CTkTextbox): input_field.delete("0.0", "end") @@ -51,9 +54,6 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): self.destroy() - - - class OptionFrame(customtkinter.CTkScrollableFrame): def add_option_label(self, text): customtkinter.CTkLabel(self, text=text, justify="left").grid( @@ -143,12 +143,16 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.number_of_options += 1 def open_date_and_time_picker(self, name, input_field): - if name not in self.date_and_time_pickers or not self.date_and_time_pickers[name].winfo_exists(): - self.date_and_time_pickers[name] = DateAndTimePickerWindow(self, input_field) + if ( + name not in self.date_and_time_pickers + or not self.date_and_time_pickers[name].winfo_exists() + ): + self.date_and_time_pickers[name] = DateAndTimePickerWindow( + self, input_field + ) else: self.date_and_time_pickers[name].focus() - def add_date_time_option(self, name, description, value): self.add_option_label(description) self.date_time_options[name] = None @@ -279,10 +283,10 @@ class SyngGui(customtkinter.CTk): self.protocol("WM_DELETE_WINDOW", self.on_close) rel_path = os.path.dirname(__file__) - img = PIL.ImageTk.PhotoImage(file=os.path.join(rel_path,"static/syng.png")) + img = PIL.ImageTk.PhotoImage(file=os.path.join(rel_path, "static/syng.png")) self.wm_iconbitmap() self.iconphoto(False, img) - + self.server = None self.client = None @@ -313,17 +317,16 @@ class SyngGui(customtkinter.CTk): ) loadbutton.pack(side="left") - startbutton = customtkinter.CTkButton( + self.startbutton = customtkinter.CTkButton( fileframe, text="Start", command=self.start_client ) - startbutton.pack(side="right") + self.startbutton.pack(side="right") startserverbutton = customtkinter.CTkButton( fileframe, text="Start Server", command=self.start_server ) startserverbutton.pack(side="right") - open_web_button = customtkinter.CTkButton( fileframe, text="Open Web", command=self.open_web ) @@ -365,21 +368,28 @@ class SyngGui(customtkinter.CTk): self.updateQr() def start_client(self): - sources = {} - for source, tab in self.tabs.items(): - sources[source] = tab.get_config() + if self.client is None: + sources = {} + for source, tab in self.tabs.items(): + sources[source] = tab.get_config() - general_config = self.general_config.get_config() + general_config = self.general_config.get_config() - config = {"sources": sources, "config": general_config} - # print(config) - self.client = multiprocessing.Process(target=create_async_and_start_client, args=(config,)) - self.client.start() + config = {"sources": sources, "config": general_config} + # print(config) + self.client = multiprocessing.Process( + target=create_async_and_start_client, args=(config,) + ) + self.client.start() + self.startbutton.configure(text="Stop") + else: + self.client.terminate() + self.client = None + self.startbutton.configure(text="Start") def start_server(self): self.server = multiprocessing.Process(target=server_main) self.server.start() - def open_web(self): config = self.general_config.get_config() diff --git a/syng/sources/__init__.py b/syng/sources/__init__.py index 976615f..c57d3a3 100644 --- a/syng/sources/__init__.py +++ b/syng/sources/__init__.py @@ -26,6 +26,6 @@ def configure_sources(configs: dict[str, Any]) -> dict[str, Source]: configured_sources = {} for source, config in configs.items(): if source in available_sources: - if config["enabled"]: + if "enabled" in config and config["enabled"]: configured_sources[source] = available_sources[source](config) return configured_sources -- 2.45.3 From 631a408adad039171513efbc2fd53d8aac4dc6d3 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Wed, 15 Nov 2023 02:17:55 +0100 Subject: [PATCH 12/18] Client stopping stops now also the mpv process --- syng/client.py | 22 ++++++++++++++++++---- syng/gui.py | 8 ++++---- syng/sources/files.py | 3 ++- syng/sources/s3.py | 1 + 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/syng/client.py b/syng/client.py index af7a55f..48d216e 100644 --- a/syng/client.py +++ b/syng/client.py @@ -48,6 +48,7 @@ from typing import Optional import qrcode import socketio +import engineio from PIL import Image from . import jsonencoder @@ -375,8 +376,11 @@ async def handle_request_config(data: dict[str, Any]) -> None: await sio.emit("config", {"source": data["source"], "config": config}) -def terminate(*args): - print("OOPS") +def signal_handler(): + engineio.async_client.async_signal_handler() + if state.current_source is not None: + if state.current_source.player is not None: + state.current_source.player.kill() async def start_client(config: dict[str, Any]) -> None: @@ -408,8 +412,18 @@ async def start_client(config: dict[str, Any]) -> None: state.config["key"] = "" await sio.connect(state.config["server"]) - await sio.wait() - print("exit") + + asyncio.get_event_loop().add_signal_handler(signal.SIGINT, signal_handler) + asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, signal_handler) + + try: + await sio.wait() + except asyncio.CancelledError: + pass + finally: + if state.current_source is not None: + if state.current_source.player is not None: + state.current_source.player.kill() def create_async_and_start_client(config): diff --git a/syng/gui.py b/syng/gui.py index 7a1cf19..f248919 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -322,10 +322,10 @@ class SyngGui(customtkinter.CTk): ) self.startbutton.pack(side="right") - startserverbutton = customtkinter.CTkButton( - fileframe, text="Start Server", command=self.start_server - ) - startserverbutton.pack(side="right") + # startserverbutton = customtkinter.CTkButton( + # fileframe, text="Start Server", command=self.start_server + # ) + # startserverbutton.pack(side="right") open_web_button = customtkinter.CTkButton( fileframe, text="Open Web", command=self.open_web diff --git a/syng/sources/files.py b/syng/sources/files.py index f5775e2..0d0101a 100644 --- a/syng/sources/files.py +++ b/syng/sources/files.py @@ -18,7 +18,8 @@ class FilesSource(FileBasedSource): source_name = "files" config_schema = FileBasedSource.config_schema | { - "dir": (str, "Directory to index", ".") + "dir": (str, "Directory to index", "."), + "index_file": (str, "Index file", "files-index"), } def __init__(self, config: dict[str, Any]): diff --git a/syng/sources/s3.py b/syng/sources/s3.py index 5147753..f3c103b 100644 --- a/syng/sources/s3.py +++ b/syng/sources/s3.py @@ -41,6 +41,7 @@ class S3Source(FileBasedSource): "secure": (bool, "Use SSL", True), "bucket": (str, "Bucket of the s3", ""), "tmp_dir": (str, "Folder for\ntemporary download", "/tmp/syng"), + "index_file": (str, "Index file", "s3-index"), } def __init__(self, config: dict[str, Any]): -- 2.45.3 From 64fa5a172079307824a623da0b99cef19d5fb8ef Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Wed, 15 Nov 2023 16:40:17 +0100 Subject: [PATCH 13/18] Date and time is now prefilled in datetimepicker. I hate am/pm. --- syng/gui.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/syng/gui.py b/syng/gui.py index f248919..4c735be 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -25,12 +25,30 @@ from .server import main as server_main class DateAndTimePickerWindow(customtkinter.CTkToplevel): def __init__(self, parent, input_field): super().__init__(parent) + + try: + iso_string = input_field.get("0.0", "end").strip() + selected = datetime.fromisoformat(iso_string) + except ValueError: + selected = datetime.now() + self.calendar = Calendar(self) - self.calendar.pack(expand=True, fill="both") - self.timepicker = AnalogPicker(self, type=constants.HOURS12) + self.calendar.pack( + expand=True, + fill="both", + ) + self.timepicker = AnalogPicker( + self, + type=constants.HOURS12, + period=constants.AM if selected.hour < 12 else constants.PM, + ) theme = AnalogThemes(self.timepicker) theme.setDracula() - # self.timepicker.addAll(constants.HOURS24) + + self.calendar.selection_set(selected) + self.timepicker.setHours(selected.hour % 12) + self.timepicker.setMinutes(selected.minute) + self.timepicker.pack(expand=True, fill="both") button = customtkinter.CTkButton( @@ -41,12 +59,12 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): def insert(self, input_field: customtkinter.CTkTextbox): input_field.delete("0.0", "end") selected_date = self.calendar.selection_get() - print(type(selected_date)) if not isinstance(selected_date, date): return hours, minutes, ampm = self.timepicker.time() + hours = hours % 12 if ampm == "PM": - hours = (hours + 12) % 24 + hours = hours + 12 selected_datetime = datetime.combine(selected_date, time(hours, minutes)) input_field.insert("0.0", selected_datetime.isoformat()) @@ -376,7 +394,6 @@ class SyngGui(customtkinter.CTk): general_config = self.general_config.get_config() config = {"sources": sources, "config": general_config} - # print(config) self.client = multiprocessing.Process( target=create_async_and_start_client, args=(config,) ) -- 2.45.3 From 65eb9bd7bf8674fd8bcc499df8fdb9090346efa4 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Wed, 15 Nov 2023 17:01:50 +0100 Subject: [PATCH 14/18] started typing gui --- syng/gui.py | 107 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/syng/gui.py b/syng/gui.py index 4c735be..ade1253 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,8 +1,10 @@ import asyncio +from collections.abc import Callable from datetime import datetime, date, time import os import builtins from functools import partial +from typing import Any, Optional import webbrowser import PIL from yaml import load, Loader @@ -11,7 +13,7 @@ import customtkinter import qrcode import secrets import string -from tkinter import PhotoImage, filedialog +from tkinter import PhotoImage, Tk, filedialog from tkcalendar import Calendar from tktimepicker import SpinTimePickerOld, AnalogPicker, AnalogThemes from tktimepicker import constants @@ -23,7 +25,11 @@ from .server import main as server_main class DateAndTimePickerWindow(customtkinter.CTkToplevel): - def __init__(self, parent, input_field): + def __init__( + self, + parent: customtkinter.CTkFrame | customtkinter.CTkScrollableFrame, + input_field: customtkinter.CTkTextbox, + ) -> None: super().__init__(parent) try: @@ -56,7 +62,7 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): ) button.pack(expand=True, fill="x") - def insert(self, input_field: customtkinter.CTkTextbox): + def insert(self, input_field: customtkinter.CTkTextbox) -> None: input_field.delete("0.0", "end") selected_date = self.calendar.selection_get() if not isinstance(selected_date, date): @@ -73,12 +79,12 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): class OptionFrame(customtkinter.CTkScrollableFrame): - def add_option_label(self, text): + def add_option_label(self, text: str) -> None: customtkinter.CTkLabel(self, text=text, justify="left").grid( column=0, row=self.number_of_options, padx=5, pady=5, sticky="ne" ) - def add_bool_option(self, name, description, value=False): + def add_bool_option(self, name: str, description: str, value: bool = False) -> None: self.add_option_label(description) self.bool_options[name] = customtkinter.CTkCheckBox( self, @@ -93,7 +99,13 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.bool_options[name].grid(column=1, row=self.number_of_options, sticky="EW") self.number_of_options += 1 - def add_string_option(self, name, description, value="", callback=None): + def add_string_option( + self, + name: str, + description: str, + value: str = "", + callback: Optional[Callable[..., None]] = None, + ): self.add_option_label(description) if value is None: value = "" @@ -110,11 +122,22 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.string_options[name].bind("", callback) self.number_of_options += 1 - def del_list_element(self, name, element, frame): + def del_list_element( + self, + name: str, + element: customtkinter.CTkTextbox, + frame: customtkinter.CTkFrame, + ) -> None: self.list_options[name].remove(element) frame.destroy() - def add_list_element(self, name, frame, init, callback): + def add_list_element( + self, + name: str, + frame: customtkinter.CTkFrame, + init: str, + callback: Optional[Callable[..., None]], + ) -> None: input_and_minus = customtkinter.CTkFrame(frame) input_and_minus.pack(side="top", fill="x", expand=True) input_field = customtkinter.CTkTextbox(input_and_minus, wrap="none", height=1) @@ -133,7 +156,13 @@ class OptionFrame(customtkinter.CTkScrollableFrame): minus_button.pack(side="right") self.list_options[name].append(input_field) - def add_list_option(self, name, description, value=[], callback=None): + def add_list_option( + self, + name: str, + description: str, + value: list[str] = [], + callback: Optional[Callable[..., None]] = None, + ) -> None: self.add_option_label(description) frame = customtkinter.CTkFrame(self) @@ -151,7 +180,9 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.number_of_options += 1 - def add_choose_option(self, name, description, values, value=""): + def add_choose_option( + self, name: str, description: str, values: list[str], value: str = "" + ) -> None: self.add_option_label(description) self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) self.choose_options[name].grid( @@ -160,7 +191,9 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.choose_options[name].set(value) self.number_of_options += 1 - def open_date_and_time_picker(self, name, input_field): + def open_date_and_time_picker( + self, name: str, input_field: customtkinter.CTkTextbox + ) -> None: if ( name not in self.date_and_time_pickers or not self.date_and_time_pickers[name].winfo_exists() @@ -171,7 +204,7 @@ class OptionFrame(customtkinter.CTkScrollableFrame): else: self.date_and_time_pickers[name].focus() - def add_date_time_option(self, name, description, value): + def add_date_time_option(self, name: str, description: str, value: str) -> None: self.add_option_label(description) self.date_time_options[name] = None input_and_button = customtkinter.CTkFrame(self) @@ -193,7 +226,7 @@ class OptionFrame(customtkinter.CTkScrollableFrame): button.pack(side="right") self.number_of_options += 1 - def __init__(self, parent): + def __init__(self, parent: customtkinter.CTkFrame) -> None: super().__init__(parent) self.columnconfigure((1,), weight=1) self.number_of_options = 0 @@ -204,7 +237,7 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self.date_time_options = {} self.date_and_time_pickers = {} - def get_config(self): + def get_config(self) -> dict[str, Any]: config = {} for name, textbox in self.string_options.items(): config[name] = textbox.get("0.0", "end").strip() @@ -224,19 +257,9 @@ class OptionFrame(customtkinter.CTkScrollableFrame): class SourceTab(OptionFrame): - def updateStrVar(self, var: str, element: customtkinter.CTkTextbox, event): - value = element.get("0.0", "end").strip() - self.vars[var] = value - - def updateBoolVar(self, var: str, element: customtkinter.CTkCheckBox, event): - value = True if element.get() == 1 else False - self.vars[var] = value - - def updateListVar(self, var: str, element: customtkinter.CTkTextbox, event): - value = [v.strip() for v in element.get("0.0", "end").strip().split(",")] - self.vars[var] = value - - def __init__(self, parent, source_name, config): + def __init__( + self, parent: customtkinter.CTkFrame, source_name: str, config: dict[str, Any] + ) -> None: super().__init__(parent) source = available_sources[source_name] self.vars: dict[str, str | bool | list[str]] = {} @@ -252,7 +275,12 @@ class SourceTab(OptionFrame): class GeneralConfig(OptionFrame): - def __init__(self, parent, config, callback): + def __init__( + self, + parent: customtkinter.CTkFrame, + config: dict[str, Any], + callback: Callable[..., None], + ) -> None: super().__init__(parent) self.add_string_option("server", "Server", config["server"], callback) @@ -264,15 +292,12 @@ class GeneralConfig(OptionFrame): ["forced", "optional", "none"], str(config["waiting_room_policy"]).lower(), ) - # self.add_string_option( - # "last_song", "Time of last song\nin ISO-8601", config["last_song"] - # ) self.add_date_time_option("last_song", "Time of last song", config["last_song"]) self.add_string_option( "preview_duration", "Preview Duration", config["preview_duration"] ) - def get_config(self): + def get_config(self) -> dict[str, Any]: config = super().get_config() try: config["preview_duration"] = int(config["preview_duration"]) @@ -283,10 +308,10 @@ class GeneralConfig(OptionFrame): class SyngGui(customtkinter.CTk): - def loadConfig(self): + def loadConfig(self) -> None: filedialog.askopenfilename() - def on_close(self): + def on_close(self) -> None: if self.server is not None: self.server.kill() @@ -296,7 +321,7 @@ class SyngGui(customtkinter.CTk): self.withdraw() self.destroy() - def __init__(self): + def __init__(self) -> None: super().__init__(className="Syng") self.protocol("WM_DELETE_WINDOW", self.on_close) @@ -385,7 +410,7 @@ class SyngGui(customtkinter.CTk): self.updateQr() - def start_client(self): + def start_client(self) -> None: if self.client is None: sources = {} for source, tab in self.tabs.items(): @@ -404,18 +429,18 @@ class SyngGui(customtkinter.CTk): self.client = None self.startbutton.configure(text="Start") - def start_server(self): + def start_server(self) -> None: self.server = multiprocessing.Process(target=server_main) self.server.start() - def open_web(self): + def open_web(self) -> None: config = self.general_config.get_config() server = config["server"] server += "" if server.endswith("/") else "/" room = config["room"] webbrowser.open(server + room) - def changeQr(self, data: str): + def changeQr(self, data: str) -> None: qr = qrcode.QRCode(box_size=20, border=2) qr.add_data(data) qr.make() @@ -424,7 +449,7 @@ class SyngGui(customtkinter.CTk): tkQrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) self.qrlabel.configure(image=tkQrcode) - def updateQr(self, _evt=None): + def updateQr(self, _evt: None = None) -> None: config = self.general_config.get_config() server = config["server"] server += "" if server.endswith("/") else "/" @@ -433,7 +458,7 @@ class SyngGui(customtkinter.CTk): self.changeQr(server + room) -def main(): +def main() -> None: SyngGui().mainloop() -- 2.45.3 From 14821ab759ff4998e259f833755a6994a712713d Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Mon, 18 Dec 2023 18:56:03 +0100 Subject: [PATCH 15/18] Preparation for 2.0 release --- customtkinter.pyi | 66 ++++++++++++ docs/source/conf.py | 5 +- pyproject.toml | 22 ++-- syng/client.py | 39 +++---- syng/gui.py | 208 +++++++++++++++++++------------------- syng/py.typed | 0 syng/server.py | 88 +++++----------- syng/sources/filebased.py | 3 +- syng/sources/s3.py | 2 +- syng/sources/source.py | 10 +- 10 files changed, 231 insertions(+), 212 deletions(-) create mode 100644 customtkinter.pyi create mode 100644 syng/py.typed diff --git a/customtkinter.pyi b/customtkinter.pyi new file mode 100644 index 0000000..d0b5aeb --- /dev/null +++ b/customtkinter.pyi @@ -0,0 +1,66 @@ +from tkinter import Tk +from typing import Any, Callable, Optional + +from PIL.Image import Image + +class CTk(Tk): + def __init__(self, parent: Optional[Tk] = None, className: str = "Tk") -> None: + pass + def pack( + self, + expand: bool = False, + fill: str = "", + side: str = "", + padx: int = 0, + pady: int = 0, + ipadx: int = 0, + ipady: int = 0, + anchor: str = "", + ) -> None: ... + def grid( + self, column: int, row: int, padx: int = 0, pady: int = 0, sticky: str = "" + ) -> None: ... + def configure(self, **kwargs: Any) -> None: ... + +class CTkToplevel(CTk): ... +class CTkFrame(CTk): ... + +class CTkImage: + def __init__(self, light_image: Image, size: tuple[int, int]) -> None: ... + +class CTkTabview(CTk): + def __init__(self, parent: Tk, width: int, height: int) -> None: ... + def add(self, name: str) -> None: ... + def set(self, name: str) -> None: ... + def tab(self, name: str) -> CTkFrame: ... + +class CTkOptionMenu(CTk): + def __init__(self, parent: Tk, values: list[str]) -> None: ... + def set(self, value: str) -> None: ... + def get(self) -> str: ... + +class CTkCheckBox(CTk): + def __init__(self, parent: Tk, text: str, onvalue: Any, offvalue: Any) -> None: ... + def select(self) -> None: ... + def deselect(self) -> None: ... + def get(self) -> Any: ... + +class CTkLabel(CTk): + def __init__(self, parent: Tk, text: str, justify: str = "") -> None: ... + +class CTkTextbox(CTk): + def __init__(self, parent: Tk, wrap: str = "none", height: int = 1) -> None: ... + def get(self, start: str, end: str) -> str: ... + def delete(self, start: str, end: str) -> None: ... + def insert(self, start: str, value: str) -> None: ... + +class CTkScrollableFrame(CTk): ... + +class CTkButton(CTk): + def __init__( + self, + parent: Tk, + text: str, + command: Callable[..., None], + width: Optional[int] = None, + ) -> None: ... diff --git a/docs/source/conf.py b/docs/source/conf.py index 3246357..0c1c1b3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,6 +6,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import sys +import sphinx_rtd_theme sys.path.insert(0, os.path.abspath("..")) @@ -17,7 +18,7 @@ release = "2.0.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx.ext.autodoc"] +extensions = ["sphinx.ext.autodoc", "sphinx_rtd_theme"] templates_path = ["_templates"] exclude_patterns = [] @@ -26,5 +27,5 @@ exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "alabaster" +html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] diff --git a/pyproject.toml b/pyproject.toml index 7b7f246..c475383 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ pyyaml = "^6.0.1" async-tkinter-loop = "^0.9.2" tkcalendar = "^1.6.1" tktimepicker = "^2.0.2" +types-pyyaml = "^6.0.12.12" +types-pillow = "^10.1.0.2" +platformdirs = "^4.0.0" [build-system] requires = ["poetry-core"] @@ -40,17 +43,20 @@ exclude = [ ".venv" ] venvPath = "." venv = ".venv" +[tool.pylint."MESSAGES CONTROL"] +disable = '''too-many-lines, +too-many-ancestors +''' + [[tool.mypy.overrides]] module = [ - "aiohttp", - "pytube", - "minio", - "aiocmd", - "pyqrcodeng", - "socketio", - "pillow", - "PIL", "yt_dlp", + "pymediainfo", + "minio", + "qrcode", + "engineio", + "tkcalendar", + "tktimepicker" ] ignore_missing_imports = true diff --git a/syng/client.py b/syng/client.py index 48d216e..3017eb6 100644 --- a/syng/client.py +++ b/syng/client.py @@ -16,19 +16,16 @@ Excerp from the help:: --config-file CONFIG_FILE, -C CONFIG_FILE --key KEY, -k KEY -The config file should be a json file in the following style:: +The config file should be a yaml file in the following style:: - { - "sources": { - "SOURCE1": { configuration for SOURCE }, - "SOURCE2": { configuration for SOURCE }, + sources: + SOURCE1: + configuration for SOURCE + SOURCE2: + configuration for SOURCE ... - }, - }, - "config": { + config: configuration for the client - } - } """ import asyncio import datetime @@ -40,21 +37,19 @@ import signal from argparse import ArgumentParser from dataclasses import dataclass from dataclasses import field -from yaml import load, Loader from traceback import print_exc -from typing import Any -from typing import Optional +from typing import Any, Optional import qrcode import socketio import engineio from PIL import Image +from yaml import load, Loader from . import jsonencoder from .entry import Entry -from .sources import configure_sources -from .sources import Source +from .sources import configure_sources, Source sio: socketio.AsyncClient = socketio.AsyncClient(json=jsonencoder) @@ -106,7 +101,7 @@ class State: * `last_song` (`Optional[datetime.datetime]`): A timestamp, defining the end of the queue. * `waiting_room_policy` (Optional[str]): One of: - - `force`, if a performer is already in the queue, they are put in the + - `forced`, if a performer is already in the queue, they are put in the waiting room. - `optional`, if a performer is already in the queue, they have the option to be put in the waiting room. @@ -200,12 +195,8 @@ async def handle_connect() -> None: "queue": state.queue, "waiting_room": state.waiting_room, "recent": state.recent, - # "room": state.config["room"], - # "secret": state.config["secret"], "config": state.config, } - if state.config["key"]: - data["registration-key"] = state.config["key"] # TODO: unify await sio.emit("register-client", data) @@ -357,9 +348,7 @@ async def handle_request_config(data: dict[str, Any]) -> None: :rtype: None """ if data["source"] in sources: - config: dict[str, Any] | list[dict[str, Any]] = await sources[ - data["source"] - ].get_config() + config: dict[str, Any] | list[dict[str, Any]] = await sources[data["source"]].get_config() if isinstance(config, list): num_chunks: int = len(config) for current, chunk in enumerate(config): @@ -376,7 +365,7 @@ async def handle_request_config(data: dict[str, Any]) -> None: await sio.emit("config", {"source": data["source"], "config": config}) -def signal_handler(): +def signal_handler() -> None: engineio.async_client.async_signal_handler() if state.current_source is not None: if state.current_source.player is not None: @@ -426,7 +415,7 @@ async def start_client(config: dict[str, Any]) -> None: state.current_source.player.kill() -def create_async_and_start_client(config): +def create_async_and_start_client(config: dict[str, Any]) -> None: asyncio.run(start_client(config)) diff --git a/syng/gui.py b/syng/gui.py index ade1253..e6ec034 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,4 +1,4 @@ -import asyncio +from multiprocessing import Process from collections.abc import Callable from datetime import datetime, date, time import os @@ -6,19 +6,19 @@ import builtins from functools import partial from typing import Any, Optional import webbrowser -import PIL -from yaml import load, Loader import multiprocessing -import customtkinter -import qrcode import secrets import string -from tkinter import PhotoImage, Tk, filedialog -from tkcalendar import Calendar -from tktimepicker import SpinTimePickerOld, AnalogPicker, AnalogThemes -from tktimepicker import constants -from .client import create_async_and_start_client, default_config, start_client +from PIL import ImageTk +from yaml import dump, load, Loader, Dumper +import customtkinter +from qrcode import QRCode +from tkcalendar import Calendar +from tktimepicker import AnalogPicker, AnalogThemes, constants +import platformdirs + +from .client import create_async_and_start_client, default_config from .sources import available_sources from .server import main as server_main @@ -57,9 +57,7 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): self.timepicker.pack(expand=True, fill="both") - button = customtkinter.CTkButton( - self, text="Ok", command=partial(self.insert, input_field) - ) + button = customtkinter.CTkButton(self, text="Ok", command=partial(self.insert, input_field)) button.pack(expand=True, fill="x") def insert(self, input_field: customtkinter.CTkTextbox) -> None: @@ -105,17 +103,13 @@ class OptionFrame(customtkinter.CTkScrollableFrame): description: str, value: str = "", callback: Optional[Callable[..., None]] = None, - ): + ) -> None: self.add_option_label(description) if value is None: value = "" - self.string_options[name] = customtkinter.CTkTextbox( - self, wrap="none", height=1 - ) - self.string_options[name].grid( - column=1, row=self.number_of_options, sticky="EW" - ) + self.string_options[name] = customtkinter.CTkTextbox(self, wrap="none", height=1) + self.string_options[name].grid(column=1, row=self.number_of_options, sticky="EW") self.string_options[name].insert("0.0", value) if callback is not None: self.string_options[name].bind("", callback) @@ -160,7 +154,7 @@ class OptionFrame(customtkinter.CTkScrollableFrame): self, name: str, description: str, - value: list[str] = [], + value: list[str], callback: Optional[Callable[..., None]] = None, ) -> None: self.add_option_label(description) @@ -185,32 +179,26 @@ class OptionFrame(customtkinter.CTkScrollableFrame): ) -> None: self.add_option_label(description) self.choose_options[name] = customtkinter.CTkOptionMenu(self, values=values) - self.choose_options[name].grid( - column=1, row=self.number_of_options, sticky="EW" - ) + self.choose_options[name].grid(column=1, row=self.number_of_options, sticky="EW") self.choose_options[name].set(value) self.number_of_options += 1 - def open_date_and_time_picker( - self, name: str, input_field: customtkinter.CTkTextbox - ) -> None: + def open_date_and_time_picker(self, name: str, input_field: customtkinter.CTkTextbox) -> None: if ( name not in self.date_and_time_pickers or not self.date_and_time_pickers[name].winfo_exists() ): - self.date_and_time_pickers[name] = DateAndTimePickerWindow( - self, input_field - ) + self.date_and_time_pickers[name] = DateAndTimePickerWindow(self, input_field) else: self.date_and_time_pickers[name].focus() def add_date_time_option(self, name: str, description: str, value: str) -> None: self.add_option_label(description) - self.date_time_options[name] = None input_and_button = customtkinter.CTkFrame(self) input_and_button.grid(column=1, row=self.number_of_options, sticky="EW") input_field = customtkinter.CTkTextbox(input_and_button, wrap="none", height=1) input_field.pack(side="left", fill="x", expand=True) + self.date_time_options[name] = input_field try: datetime.fromisoformat(value) except TypeError: @@ -229,16 +217,16 @@ class OptionFrame(customtkinter.CTkScrollableFrame): def __init__(self, parent: customtkinter.CTkFrame) -> None: super().__init__(parent) self.columnconfigure((1,), weight=1) - self.number_of_options = 0 - self.string_options = {} - self.choose_options = {} - self.bool_options = {} - self.list_options = {} - self.date_time_options = {} - self.date_and_time_pickers = {} + self.number_of_options: int = 0 + self.string_options: dict[str, customtkinter.CTkTextbox] = {} + self.choose_options: dict[str, customtkinter.CTkOptionMenu] = {} + self.bool_options: dict[str, customtkinter.CTkCheckBox] = {} + self.list_options: dict[str, list[customtkinter.CTkTextbox]] = {} + self.date_time_options: dict[str, customtkinter.CTkTextbox] = {} + self.date_and_time_pickers: dict[str, DateAndTimePickerWindow] = {} def get_config(self) -> dict[str, Any]: - config = {} + config: dict[str, Any] = {} for name, textbox in self.string_options.items(): config[name] = textbox.get("0.0", "end").strip() @@ -253,6 +241,9 @@ class OptionFrame(customtkinter.CTkScrollableFrame): for textbox in textboxes: config[name].append(textbox.get("0.0", "end").strip()) + for name, picker in self.date_time_options.items(): + config[name] = picker.get("0.0", "end").strip() + return config @@ -293,9 +284,7 @@ class GeneralConfig(OptionFrame): str(config["waiting_room_policy"]).lower(), ) self.add_date_time_option("last_song", "Time of last song", config["last_song"]) - self.add_string_option( - "preview_duration", "Preview Duration", config["preview_duration"] - ) + self.add_string_option("preview_duration", "Preview Duration", config["preview_duration"]) def get_config(self) -> dict[str, Any]: config = super().get_config() @@ -308,15 +297,12 @@ class GeneralConfig(OptionFrame): class SyngGui(customtkinter.CTk): - def loadConfig(self) -> None: - filedialog.askopenfilename() - def on_close(self) -> None: - if self.server is not None: - self.server.kill() + if self.syng_server is not None: + self.syng_server.kill() - if self.client is not None: - self.client.kill() + if self.syng_client is not None: + self.syng_client.kill() self.withdraw() self.destroy() @@ -326,21 +312,30 @@ class SyngGui(customtkinter.CTk): self.protocol("WM_DELETE_WINDOW", self.on_close) rel_path = os.path.dirname(__file__) - img = PIL.ImageTk.PhotoImage(file=os.path.join(rel_path, "static/syng.png")) + img = ImageTk.PhotoImage(file=os.path.join(rel_path, "static/syng.png")) self.wm_iconbitmap() self.iconphoto(False, img) - self.server = None - self.client = None + self.syng_server: Optional[Process] = None + self.syng_client: Optional[Process] = None + + self.configfile = os.path.join(platformdirs.user_config_dir("syng"), "config.yaml") try: - with open("syng-client.yaml") as cfile: + with open(self.configfile, encoding="utf8") as cfile: loaded_config = load(cfile, Loader=Loader) except FileNotFoundError: + print("No config found, using default values") loaded_config = {} - config = {"sources": {}, "config": default_config()} - if "config" in loaded_config: + config: dict[str, dict[str, Any]] = {"sources": {}, "config": default_config()} + + try: config["config"] |= loaded_config["config"] + except (KeyError, TypeError): + print("Could not load config") + + # if "config" in loaded_config: + # config["config"] |= loaded_config["config"] if not config["config"]["secret"]: config["config"]["secret"] = "".join( @@ -350,30 +345,26 @@ class SyngGui(customtkinter.CTk): self.wm_title("Syng") # Buttons - fileframe = customtkinter.CTkFrame(self) - fileframe.pack(side="bottom") + button_line = customtkinter.CTkFrame(self) + button_line.pack(side="bottom", fill="x") - loadbutton = customtkinter.CTkButton( - fileframe, - text="load", - command=self.loadConfig, + startsyng_serverbutton = customtkinter.CTkButton( + button_line, text="Start Local Server", command=self.start_syng_server ) - loadbutton.pack(side="left") + startsyng_serverbutton.pack(side="left", expand=True, anchor="w", padx=10, pady=5) - self.startbutton = customtkinter.CTkButton( - fileframe, text="Start", command=self.start_client - ) - self.startbutton.pack(side="right") - - # startserverbutton = customtkinter.CTkButton( - # fileframe, text="Start Server", command=self.start_server - # ) - # startserverbutton.pack(side="right") + savebutton = customtkinter.CTkButton(button_line, text="Save", command=self.save_config) + savebutton.pack(side="left", padx=10, pady=5) open_web_button = customtkinter.CTkButton( - fileframe, text="Open Web", command=self.open_web + button_line, text="Open Web", command=self.open_web ) - open_web_button.pack(side="left") + open_web_button.pack(side="left", pady=5) + + self.startbutton = customtkinter.CTkButton( + button_line, text="Save and Start", command=self.start_syng_client + ) + self.startbutton.pack(side="left", padx=10, pady=10) # Tabs and QR Code frm = customtkinter.CTkFrame(self) @@ -391,7 +382,7 @@ class SyngGui(customtkinter.CTk): self.qrlabel.pack(side="left", anchor="n", padx=10, pady=10) self.general_config = GeneralConfig( - tabview.tab("General"), config["config"], self.updateQr + tabview.tab("General"), config["config"], self.update_qr ) self.general_config.pack(ipadx=10, fill="both", expand=True) @@ -400,62 +391,67 @@ class SyngGui(customtkinter.CTk): for source_name in available_sources: try: source_config = loaded_config["sources"][source_name] - except KeyError: + except (KeyError, TypeError): source_config = {} - self.tabs[source_name] = SourceTab( - tabview.tab(source_name), source_name, source_config - ) + self.tabs[source_name] = SourceTab(tabview.tab(source_name), source_name, source_config) self.tabs[source_name].pack(ipadx=10, expand=True, fill="both") - self.updateQr() + self.update_qr() - def start_client(self) -> None: - if self.client is None: - sources = {} - for source, tab in self.tabs.items(): - sources[source] = tab.get_config() + def save_config(self) -> None: + with open(self.configfile, "w", encoding="utf-8") as f: + dump(self.gather_config(), f, Dumper=Dumper) - general_config = self.general_config.get_config() + def gather_config(self) -> dict[str, Any]: + sources = {} + for source, tab in self.tabs.items(): + sources[source] = tab.get_config() - config = {"sources": sources, "config": general_config} - self.client = multiprocessing.Process( + general_config = self.general_config.get_config() + + return {"sources": sources, "config": general_config} + + def start_syng_client(self) -> None: + if self.syng_client is None: + config = self.gather_config() + self.syng_client = multiprocessing.Process( target=create_async_and_start_client, args=(config,) ) - self.client.start() + self.syng_client.start() self.startbutton.configure(text="Stop") else: - self.client.terminate() - self.client = None - self.startbutton.configure(text="Start") + self.syng_client.terminate() + self.syng_client = None + self.startbutton.configure(text="Save and Start") - def start_server(self) -> None: - self.server = multiprocessing.Process(target=server_main) - self.server.start() + def start_syng_server(self) -> None: + self.syng_server = multiprocessing.Process(target=server_main) + self.syng_server.start() def open_web(self) -> None: config = self.general_config.get_config() - server = config["server"] - server += "" if server.endswith("/") else "/" + syng_server = config["server"] + syng_server += "" if syng_server.endswith("/") else "/" room = config["room"] - webbrowser.open(server + room) + webbrowser.open(syng_server + room) - def changeQr(self, data: str) -> None: - qr = qrcode.QRCode(box_size=20, border=2) + def change_qr(self, data: str) -> None: + qr = QRCode(box_size=20, border=2) qr.add_data(data) qr.make() qr.print_ascii() image = qr.make_image().convert("RGB") - tkQrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) - self.qrlabel.configure(image=tkQrcode) + tk_qrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) + self.qrlabel.configure(image=tk_qrcode) - def updateQr(self, _evt: None = None) -> None: + def update_qr(self, _evt: None = None) -> None: config = self.general_config.get_config() - server = config["server"] - server += "" if server.endswith("/") else "/" + syng_server = config["server"] + syng_server += "" if syng_server.endswith("/") else "/" room = config["room"] - print(server + room) - self.changeQr(server + room) + print(syng_server + room) + self.change_qr(syng_server + room) def main() -> None: diff --git a/syng/py.typed b/syng/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/syng/server.py b/syng/server.py index 157bf1c..7c33302 100644 --- a/syng/server.py +++ b/syng/server.py @@ -90,7 +90,7 @@ class Client: in the calculation of the ETA for songs later in the queue. * `last_song` (`Optional[float]`): A timestamp, defining the end of the queue. * `waiting_room_policy` (Optional[str]): One of: - - `force`, if a performer is already in the queue, they are put in the + - `forced`, if a performer is already in the queue, they are put in the waiting room. - `optional`, if a performer is already in the queue, they have the option to be put in the waiting room. @@ -132,9 +132,7 @@ class State: recent: list[Entry] sid: str client: Client - last_seen: datetime.datetime = field( - init=False, default_factory=datetime.datetime.now - ) + last_seen: datetime.datetime = field(init=False, default_factory=datetime.datetime.now) clients: dict[str, State] = {} @@ -156,13 +154,17 @@ async def send_state(state: State, sid: str) -> None: :type sid: str: :rtype: None """ + + safe_config = {k: v for k, v in state.client.config.items() if k not in ["secret", "key"]} + print(safe_config) + await sio.emit( "state", { "queue": state.queue, "recent": state.recent, "waiting_room": state.waiting_room, - "config": state.client.config, + "config": safe_config, }, room=sid, ) @@ -205,18 +207,13 @@ async def handle_waiting_room_append(sid: str, data: dict[str, Any]) -> None: if entry is None: await sio.emit( "msg", - { - "msg": f"Unable to add to the waiting room: {data['ident']}. Maybe try again?" - }, + {"msg": f"Unable to add to the waiting room: {data['ident']}. Maybe try again?"}, room=sid, ) return if "uid" not in data or ( - ( - data["uid"] is not None - and len(list(state.queue.find_by_uid(data["uid"]))) == 0 - ) + (data["uid"] is not None and len(list(state.queue.find_by_uid(data["uid"]))) == 0) or (data["uid"] is None and state.queue.find_by_name(data["performer"]) is None) ): await append_to_queue(room, entry, sid) @@ -233,9 +230,7 @@ async def handle_waiting_room_append(sid: str, data: dict[str, Any]) -> None: ) -async def append_to_queue( - room: str, entry: Entry, report_to: Optional[str] = None -) -> None: +async def append_to_queue(room: str, entry: Entry, report_to: Optional[str] = None) -> None: """ Append a song to the queue for a given session. @@ -259,10 +254,7 @@ async def append_to_queue( start_time = first_song.started_at start_time = state.queue.fold( - lambda item, time: time - + item.duration - + state.client.config["preview_duration"] - + 1, + lambda item, time: time + item.duration + state.client.config["preview_duration"] + 1, start_time, ) @@ -384,7 +376,7 @@ async def handle_append(sid: str, data: dict[str, Any]) -> None: state = clients[room] if state.client.config["waiting_room_policy"] and ( - state.client.config["waiting_room_policy"].lower() == "force" + state.client.config["waiting_room_policy"].lower() == "forced" or state.client.config["waiting_room_policy"].lower() == "optional" ): old_entry = state.queue.find_by_name(data["performer"]) @@ -437,7 +429,7 @@ async def handle_append_anyway(sid: str, data: dict[str, Any]) -> None: room = session["room"] state = clients[room] - if state.client.config["waiting_room_policy"].lower() == "force": + if state.client.config["waiting_room_policy"].lower() == "forced": await sio.emit( "err", {"type": "WAITING_ROOM_FORCED"}, @@ -546,11 +538,7 @@ async def handle_waiting_room_to_queue(sid: str, data: dict[str, Any]) -> None: if is_admin: entry = next( - ( - wr_entry - for wr_entry in state.waiting_room - if str(wr_entry.uuid) == data["uuid"] - ), + (wr_entry for wr_entry in state.waiting_room if str(wr_entry.uuid) == data["uuid"]), None, ) if entry is not None: @@ -682,22 +670,19 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: """ def gen_id(length: int = 4) -> str: - client_id = "".join( - [random.choice(string.ascii_letters) for _ in range(length)] - ) + client_id = "".join([random.choice(string.ascii_letters) for _ in range(length)]) if client_id in clients: client_id = gen_id(length + 1) return client_id if not app["public"]: - with open(app["registration-keyfile"]) as f: + with open(app["registration-keyfile"], encoding="utf8") as f: raw_keys = f.readlines() keys = [key[:64] for key in raw_keys] if ( - "registration-key" not in data - or hashlib.sha256(data["registration-key"].encode()).hexdigest() - not in keys + "key" not in data["config"] + or hashlib.sha256(data["config"]["key"].encode()).hexdigest() not in keys ): await sio.emit( "client-registered", @@ -707,9 +692,7 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: return room: str = ( - data["config"]["room"] - if "room" in data["config"] and data["config"]["room"] - else gen_id() + data["config"]["room"] if "room" in data["config"] and data["config"]["room"] else gen_id() ) async with sio.session(sid) as session: session["room"] = room @@ -725,15 +708,11 @@ async def handle_register_client(sid: str, data: dict[str, Any]) -> None: config=DEFAULT_CONFIG | data["config"], ) await sio.enter_room(sid, room) - await sio.emit( - "client-registered", {"success": True, "room": room}, room=sid - ) + await sio.emit("client-registered", {"success": True, "room": room}, room=sid) await send_state(clients[room], sid) else: logger.warning("Got wrong secret for %s", room) - await sio.emit( - "client-registered", {"success": False, "room": room}, room=sid - ) + await sio.emit("client-registered", {"success": False, "room": room}, room=sid) else: logger.info("Registerd new client %s", room) initial_entries = [Entry(**entry) for entry in data["queue"]] @@ -824,9 +803,7 @@ async def handle_config_chunk(sid: str, data: dict[str, Any]) -> None: return if data["source"] not in state.client.sources: - state.client.sources[data["source"]] = available_sources[data["source"]]( - data["config"] - ) + state.client.sources[data["source"]] = available_sources[data["source"]](data["config"]) else: state.client.sources[data["source"]].add_to_config(data["config"]) @@ -855,9 +832,7 @@ async def handle_config(sid: str, data: dict[str, Any]) -> None: if sid != state.sid: return - state.client.sources[data["source"]] = available_sources[data["source"]]( - data["config"] - ) + state.client.sources[data["source"]] = available_sources[data["source"]](data["config"]) @sio.on("register-web") @@ -1042,17 +1017,10 @@ async def handle_search(sid: str, data: dict[str, Any]) -> None: query = data["query"] results_list = await asyncio.gather( - *[ - state.client.sources[source].search(query) - for source in state.client.sources_prio - ] + *[state.client.sources[source].search(query) for source in state.client.sources_prio] ) - results = [ - search_result - for source_result in results_list - for search_result in source_result - ] + results = [search_result for source_result in results_list for search_result in source_result] await sio.emit( "search-results", {"results": results}, @@ -1119,7 +1087,7 @@ def main() -> None: """ parser = ArgumentParser() parser.add_argument("--host", "-H", default="localhost") - parser.add_argument("--port", "-p", default="8080") + parser.add_argument("--port", "-p", type=int, default=8080) parser.add_argument("--root-folder", "-r", default="syng/static/") parser.add_argument("--registration-keyfile", "-k", default=None) args = parser.parse_args() @@ -1131,9 +1099,7 @@ def main() -> None: app["root_folder"] = args.root_folder - app.add_routes( - [web.static("/assets/", os.path.join(app["root_folder"], "assets/"))] - ) + app.add_routes([web.static("/assets/", os.path.join(app["root_folder"], "assets/"))]) app.router.add_route("*", "/", root_handler) app.router.add_route("*", "/{room}", root_handler) diff --git a/syng/sources/filebased.py b/syng/sources/filebased.py index dc59be0..7df58e5 100644 --- a/syng/sources/filebased.py +++ b/syng/sources/filebased.py @@ -66,7 +66,8 @@ class FileBasedSource(Source): info: str | MediaInfo = MediaInfo.parse(file) if isinstance(info, str): return 180 - return info.audio_tracks[0].to_data()["duration"] // 1000 + duration: int = info.audio_tracks[0].to_data()["duration"] + return duration // 1000 video_path, audio_path = self.get_video_audio_split(path) diff --git a/syng/sources/s3.py b/syng/sources/s3.py index f3c103b..f2318a0 100644 --- a/syng/sources/s3.py +++ b/syng/sources/s3.py @@ -107,7 +107,7 @@ class S3Source(FileBasedSource): await self.ensure_playable(entry) - file_name: Optional[str] = self.downloaded_files[entry.ident].video + file_name: str = self.downloaded_files[entry.ident].video duration = await self.get_duration(file_name) diff --git a/syng/sources/source.py b/syng/sources/source.py index 2d3d734..217f8e0 100644 --- a/syng/sources/source.py +++ b/syng/sources/source.py @@ -120,9 +120,7 @@ class Source(ABC): source for documentation. :type config: dict[str, Any] """ - self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict( - DLFilesEntry - ) + self.downloaded_files: defaultdict[str, DLFilesEntry] = defaultdict(DLFilesEntry) self._masterlock: asyncio.Lock = asyncio.Lock() self.player: Optional[asyncio.subprocess.Process] = None self._index: list[str] = config["index"] if "index" in config else [] @@ -145,9 +143,7 @@ class Source(ABC): :returns: An async reference to the process :rtype: asyncio.subprocess.Process """ - args = ["--fullscreen", *options, video] + ( - [f"--audio-file={audio}"] if audio else [] - ) + args = ["--fullscreen", *options, video] + ([f"--audio-file={audio}"] if audio else []) print(f"File is {video=} and {audio=}") @@ -231,7 +227,6 @@ class Source(ABC): :returns: A Tuple of the locations for the video and the audio file. :rtype: Tuple[str, Optional[str]] """ - ... async def buffer(self, entry: Entry) -> None: """ @@ -407,7 +402,6 @@ class Source(ABC): :return: The part of the config, that should be sended to the server. :rtype: dict[str, Any] | list[dict[str, Any]] """ - print("xzy") if not self._index: self._index = [] print(f"{self.source_name}: generating index") -- 2.45.3 From cb00e62143103ee2ccaff1245f618f729db9446e Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Mon, 18 Dec 2023 18:59:25 +0100 Subject: [PATCH 16/18] better mypy checks in ci --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bc36644..b162a65 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,10 +4,11 @@ mypy: stage: test script: - pip install mypy --quiet + - mypy --install-types - mypy syng --strict ruff: stage: test script: - pip install ruff --quiet - - ruff syng \ No newline at end of file + - ruff syng -- 2.45.3 From 159a08f824fedfcce2924a5ac003e701c33ef0e8 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Mon, 18 Dec 2023 19:44:21 +0100 Subject: [PATCH 17/18] Boy I wished, that customtkinter would be correctly typed :/ --- .gitlab-ci.yml | 6 ++- customtkinter.pyi | 66 ------------------------------ pyproject.toml | 2 + aiocmd.pyi => stubs/aiocmd.pyi | 0 mutagen.pyi => stubs/mutagen.pyi | 0 pytube.pyi => stubs/pytube.pyi | 0 socketio.pyi => stubs/socketio.pyi | 0 syng/gui.py | 6 +-- 8 files changed, 9 insertions(+), 71 deletions(-) delete mode 100644 customtkinter.pyi rename aiocmd.pyi => stubs/aiocmd.pyi (100%) rename mutagen.pyi => stubs/mutagen.pyi (100%) rename pytube.pyi => stubs/pytube.pyi (100%) rename socketio.pyi => stubs/socketio.pyi (100%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b162a65..15bc157 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,10 +1,12 @@ image: python:3-alpine +variables: + MYPYPATH: "stubs/" + mypy: stage: test script: - - pip install mypy --quiet - - mypy --install-types + - pip install mypy types-Pillow types-PyYAML --quiet - mypy syng --strict ruff: diff --git a/customtkinter.pyi b/customtkinter.pyi deleted file mode 100644 index d0b5aeb..0000000 --- a/customtkinter.pyi +++ /dev/null @@ -1,66 +0,0 @@ -from tkinter import Tk -from typing import Any, Callable, Optional - -from PIL.Image import Image - -class CTk(Tk): - def __init__(self, parent: Optional[Tk] = None, className: str = "Tk") -> None: - pass - def pack( - self, - expand: bool = False, - fill: str = "", - side: str = "", - padx: int = 0, - pady: int = 0, - ipadx: int = 0, - ipady: int = 0, - anchor: str = "", - ) -> None: ... - def grid( - self, column: int, row: int, padx: int = 0, pady: int = 0, sticky: str = "" - ) -> None: ... - def configure(self, **kwargs: Any) -> None: ... - -class CTkToplevel(CTk): ... -class CTkFrame(CTk): ... - -class CTkImage: - def __init__(self, light_image: Image, size: tuple[int, int]) -> None: ... - -class CTkTabview(CTk): - def __init__(self, parent: Tk, width: int, height: int) -> None: ... - def add(self, name: str) -> None: ... - def set(self, name: str) -> None: ... - def tab(self, name: str) -> CTkFrame: ... - -class CTkOptionMenu(CTk): - def __init__(self, parent: Tk, values: list[str]) -> None: ... - def set(self, value: str) -> None: ... - def get(self) -> str: ... - -class CTkCheckBox(CTk): - def __init__(self, parent: Tk, text: str, onvalue: Any, offvalue: Any) -> None: ... - def select(self) -> None: ... - def deselect(self) -> None: ... - def get(self) -> Any: ... - -class CTkLabel(CTk): - def __init__(self, parent: Tk, text: str, justify: str = "") -> None: ... - -class CTkTextbox(CTk): - def __init__(self, parent: Tk, wrap: str = "none", height: int = 1) -> None: ... - def get(self, start: str, end: str) -> str: ... - def delete(self, start: str, end: str) -> None: ... - def insert(self, start: str, value: str) -> None: ... - -class CTkScrollableFrame(CTk): ... - -class CTkButton(CTk): - def __init__( - self, - parent: Tk, - text: str, - command: Callable[..., None], - width: Optional[int] = None, - ) -> None: ... diff --git a/pyproject.toml b/pyproject.toml index c475383..2efa229 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,8 @@ module = [ "qrcode", "engineio", "tkcalendar", + "customtkinter", + "aiohttp", "tktimepicker" ] ignore_missing_imports = true diff --git a/aiocmd.pyi b/stubs/aiocmd.pyi similarity index 100% rename from aiocmd.pyi rename to stubs/aiocmd.pyi diff --git a/mutagen.pyi b/stubs/mutagen.pyi similarity index 100% rename from mutagen.pyi rename to stubs/mutagen.pyi diff --git a/pytube.pyi b/stubs/pytube.pyi similarity index 100% rename from pytube.pyi rename to stubs/pytube.pyi diff --git a/socketio.pyi b/stubs/socketio.pyi similarity index 100% rename from socketio.pyi rename to stubs/socketio.pyi diff --git a/syng/gui.py b/syng/gui.py index e6ec034..dc963ae 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -24,7 +24,7 @@ from .sources import available_sources from .server import main as server_main -class DateAndTimePickerWindow(customtkinter.CTkToplevel): +class DateAndTimePickerWindow(customtkinter.CTkToplevel): # type: ignore def __init__( self, parent: customtkinter.CTkFrame | customtkinter.CTkScrollableFrame, @@ -76,7 +76,7 @@ class DateAndTimePickerWindow(customtkinter.CTkToplevel): self.destroy() -class OptionFrame(customtkinter.CTkScrollableFrame): +class OptionFrame(customtkinter.CTkScrollableFrame): # type:ignore def add_option_label(self, text: str) -> None: customtkinter.CTkLabel(self, text=text, justify="left").grid( column=0, row=self.number_of_options, padx=5, pady=5, sticky="ne" @@ -296,7 +296,7 @@ class GeneralConfig(OptionFrame): return config -class SyngGui(customtkinter.CTk): +class SyngGui(customtkinter.CTk): # type:ignore def on_close(self) -> None: if self.syng_server is not None: self.syng_server.kill() -- 2.45.3 From 8a4f02976f137fd35bdbd9e2cc4c18cde7ff9d49 Mon Sep 17 00:00:00 2001 From: Christoph Stahl Date: Mon, 18 Dec 2023 19:45:34 +0100 Subject: [PATCH 18/18] --amend --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2efa229..0eb8d77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,8 @@ module = [ "tkcalendar", "customtkinter", "aiohttp", - "tktimepicker" + "tktimepicker", + "platformdirs" ] ignore_missing_imports = true -- 2.45.3