diff --git a/poetry.lock b/poetry.lock index 1be0840..a7d2108 100644 --- a/poetry.lock +++ b/poetry.lock @@ -252,20 +252,6 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] -[[package]] -name = "babel" -version = "2.16.0" -description = "Internationalization utilities" -optional = true -python-versions = ">=3.8" -files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - [[package]] name = "bidict" version = "0.23.1" @@ -608,35 +594,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "customtkinter" -version = "5.2.2" -description = "Create modern looking GUIs with Python" -optional = true -python-versions = ">=3.7" -files = [ - {file = "customtkinter-5.2.2-py3-none-any.whl", hash = "sha256:14ad3e7cd3cb3b9eb642b9d4e8711ae80d3f79fb82545ad11258eeffb2e6b37c"}, - {file = "customtkinter-5.2.2.tar.gz", hash = "sha256:fd8db3bafa961c982ee6030dba80b4c2e25858630756b513986db19113d8d207"}, -] - -[package.dependencies] -darkdetect = "*" -packaging = "*" - -[[package]] -name = "darkdetect" -version = "0.8.0" -description = "Detect OS Dark Mode from Python" -optional = true -python-versions = ">=3.6" -files = [ - {file = "darkdetect-0.8.0-py3-none-any.whl", hash = "sha256:a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85"}, - {file = "darkdetect-0.8.0.tar.gz", hash = "sha256:b5428e1170263eb5dea44c25dc3895edd75e6f52300986353cd63533fe7df8b1"}, -] - -[package.extras] -macos-listener = ["pyobjc-framework-Cocoa"] - [[package]] name = "dill" version = "0.3.8" @@ -1315,6 +1272,70 @@ files = [ {file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"}, ] +[[package]] +name = "pyqt6" +version = "6.7.1" +description = "Python bindings for the Qt cross platform application toolkit" +optional = true +python-versions = ">=3.8" +files = [ + {file = "PyQt6-6.7.1-1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7f397f4b38b23b5588eb2c0933510deb953d96b1f0323a916c4839c2a66ccccc"}, + {file = "PyQt6-6.7.1-1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2f202b7941aa74e5c7e1463a6f27d9131dbc1e6cabe85571d7364f5b3de7397"}, + {file = "PyQt6-6.7.1-cp38-abi3-macosx_11_0_universal2.whl", hash = "sha256:f053378e3aef6248fa612c8afddda17f942fb63f9fe8a9aeb2a6b6b4cbb0eba9"}, + {file = "PyQt6-6.7.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0adb7914c732ad1dee46d9cec838a98cb2b11bc38cc3b7b36fbd8701ae64bf47"}, + {file = "PyQt6-6.7.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d771fa0981514cb1ee937633dfa64f14caa902707d9afffab66677f3a73e3da"}, + {file = "PyQt6-6.7.1-cp38-abi3-win_amd64.whl", hash = "sha256:fa3954698233fe286a8afc477b84d8517f0788eb46b74da69d3ccc0170d3714c"}, + {file = "PyQt6-6.7.1.tar.gz", hash = "sha256:3672a82ccd3a62e99ab200a13903421e2928e399fda25ced98d140313ad59cb9"}, +] + +[package.dependencies] +PyQt6-Qt6 = ">=6.7.0,<6.8.0" +PyQt6-sip = ">=13.8,<14" + +[[package]] +name = "pyqt6-qt6" +version = "6.7.2" +description = "The subset of a Qt installation needed by PyQt6." +optional = true +python-versions = "*" +files = [ + {file = "PyQt6_Qt6-6.7.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:065415589219a2f364aba29d6a98920bb32810286301acbfa157e522d30369e3"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f817efa86a0e8eda9152c85b73405463fbf3266299090f32bbb2266da540ead"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:05f2c7d195d316d9e678a92ecac0252a24ed175bd2444cc6077441807d756580"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:fc93945eaef4536d68bd53566535efcbe78a7c05c2a533790a8fd022bac8bfaa"}, + {file = "PyQt6_Qt6-6.7.2-py3-none-win_amd64.whl", hash = "sha256:b2d7e5ddb1b9764cd60f1d730fa7bf7a1f0f61b2630967c81761d3d0a5a8a2e0"}, +] + +[[package]] +name = "pyqt6-sip" +version = "13.8.0" +description = "The sip module support for PyQt6" +optional = true +python-versions = ">=3.8" +files = [ + {file = "PyQt6_sip-13.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cedd554c643e54c4c2e12b5874781a87441a1b405acf3650a4a2e1df42aae231"}, + {file = "PyQt6_sip-13.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f57275b5af774529f9838adcfb58869ba3ebdaf805daea113bb0697a96a3f3cb"}, + {file = "PyQt6_sip-13.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:835ed22eab977f75fd77e60d4ff308a1fa794b1d0c04849311f36d2a080cdf3b"}, + {file = "PyQt6_sip-13.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d8b22a6850917c68ce83fc152a8b606ecb2efaaeed35be53110468885d6cdd9d"}, + {file = "PyQt6_sip-13.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b203b6fbae4a8f2d27f35b7df46200057033d9ecd9134bcf30e3eab66d43572c"}, + {file = "PyQt6_sip-13.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beaddc1ec96b342f4e239702f91802706a80cb403166c2da318cec4ad8b790cb"}, + {file = "PyQt6_sip-13.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5c086b7c9c7996ea9b7522646cc24eebbf3591ec9dd38f65c0a3fdb0dbeaac7"}, + {file = "PyQt6_sip-13.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd168667addf01f8a4b0fa7755323e43e4cd12ca4bade558c61f713a5d48ba1a"}, + {file = "PyQt6_sip-13.8.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:33d9b399fc9c9dc99496266842b0fb2735d924604774e97cf9b555667cc0fc59"}, + {file = "PyQt6_sip-13.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:056af69d1d8d28d5968066ec5da908afd82fc0be07b67cf2b84b9f02228416ce"}, + {file = "PyQt6_sip-13.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:08dd81037a2864982ece2bf9891f3bf4558e247034e112993ea1a3fe239458cb"}, + {file = "PyQt6_sip-13.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbb249b82c53180f1420571ece5dc24fea1188ba435923edd055599dffe7abfb"}, + {file = "PyQt6_sip-13.8.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:6bce6bc5870d9e87efe5338b1ee4a7b9d7d26cdd16a79a5757d80b6f25e71edc"}, + {file = "PyQt6_sip-13.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd81144b0770084e8005d3a121c9382e6f9bc8d0bb320dd618718ffe5090e0e6"}, + {file = "PyQt6_sip-13.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:755beb5d271d081e56618fb30342cdd901464f721450495cb7cb0212764da89e"}, + {file = "PyQt6_sip-13.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a0bbc0918eab5b6351735d40cf22cbfa5aa2476b55e0d5fe881aeed7d871c29"}, + {file = "PyQt6_sip-13.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7f84c472afdc7d316ff683f63129350d645ef82d9b3fd75a609b08472d1f7291"}, + {file = "PyQt6_sip-13.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1bf29e95f10a8a00819dac804ca7e5eba5fc1769adcd74c837c11477bf81954"}, + {file = "PyQt6_sip-13.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9ea9223c94906efd68148f12ae45b51a21d67e86704225ddc92bce9c54e4d93c"}, + {file = "PyQt6_sip-13.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:2559afa68825d08de09d71c42f3b6ad839dcc30f91e7c6d0785e07830d5541a5"}, + {file = "PyQt6_sip-13.8.0.tar.gz", hash = "sha256:2f74cf3d6d9cab5152bd9f49d570b2dfb87553ebb5c4919abfde27f5b9fd69d4"}, +] + [[package]] name = "python-engineio" version = "4.9.1" @@ -1575,31 +1596,6 @@ files = [ {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, ] -[[package]] -name = "tkcalendar" -version = "1.6.1" -description = "Calendar and DateEntry widgets for Tkinter" -optional = true -python-versions = "*" -files = [ - {file = "tkcalendar-1.6.1-py3-none-any.whl", hash = "sha256:9d3a80816a7b32d64fab696fa3d2a007fb23c87953267d5e343a38ff4cd7c15c"}, - {file = "tkcalendar-1.6.1.tar.gz", hash = "sha256:5edf958c0a59429e90309e9b805b2e229192bbcab952460247204d7030eea5cf"}, -] - -[package.dependencies] -babel = "*" - -[[package]] -name = "tktimepicker" -version = "2.0.2" -description = "This package provides you with easy to customize timepickers" -optional = true -python-versions = ">=3.6" -files = [ - {file = "tkTimePicker-2.0.2-py3-none-any.whl", hash = "sha256:1c8232dcf1314815414a6c9cb69b1e277cc289d5989952ff98e61ba18a9c3150"}, - {file = "tkTimePicker-2.0.2.tar.gz", hash = "sha256:b8a0d7137f6c660f9886d2cd0141c13bc334ba92578e09c3b75f3c46728066ef"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -1918,10 +1914,10 @@ static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.5.0,<0.6.0)"] test = ["pytest (>=8.1,<9.0)"] [extras] -client = ["customtkinter", "minio", "mutagen", "packaging", "pillow", "platformdirs", "pymediainfo", "pyyaml", "qrcode", "tkcalendar", "tktimepicker"] +client = ["minio", "mutagen", "packaging", "pillow", "platformdirs", "pymediainfo", "pyqt6", "pyyaml", "qrcode"] server = ["alt-profanity-check"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "3310779a7a61511f14b32d0008db391c5eb0200c67187e61fd4d46cd1ba17677" +content-hash = "2c3d2e35ed8bf2563a078971cede2cc5ba60f9802631ee6f1f5761777dc7e3a5" diff --git a/pyproject.toml b/pyproject.toml index 5727e81..0d6ca93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,15 +18,13 @@ yt-dlp = { version = "*"} minio = { version = "^7.2.0", optional = true } mutagen = { version = "^1.47.0", optional = true } pillow = { version = "^10.1.0", optional = true} -customtkinter = { version = "^5.2.1", optional = true} qrcode = { version = "^7.4.2", optional = true } pymediainfo = { version = "^6.1.0", optional = true } pyyaml = { version = "^6.0.1", optional = true } -tkcalendar = { version = "^1.6.1", optional = true } -tktimepicker = { version = "^2.0.2", optional = true } platformdirs = { version = "^4.0.0", optional = true } packaging = {version = "^23.2", optional = true} alt-profanity-check = {version = "^1.4.1", optional = true} +pyqt6 = {version = "^6.7.1", optional = true} [tool.poetry.group.dev.dependencies] types-pyyaml = "^6.0.12.12" @@ -36,9 +34,9 @@ pylint = "^3.2.7" [tool.poetry.extras] -client = ["minio", "mutagen", "pillow", "customtkinter", "qrcode", - "pymediainfo", "pyyaml", "tkcalendar", "tktimepicker", "platformdirs", - "packaging"] +client = ["minio", "mutagen", "pillow", "qrcode", + "pymediainfo", "pyyaml", "platformdirs", + "packaging", "pyqt6"] server = ["alt-profanity-check"] [build-system] diff --git a/syng/gui.py b/syng/gui.py index 7ecb36d..29f4d31 100644 --- a/syng/gui.py +++ b/syng/gui.py @@ -1,7 +1,8 @@ from argparse import Namespace +from io import BytesIO from multiprocessing import Process from collections.abc import Callable -from datetime import datetime, date, time +from datetime import datetime import os import builtins from functools import partial @@ -11,12 +12,26 @@ import multiprocessing import secrets import string -from PIL import ImageTk +from PyQt6.QtGui import QCloseEvent, QIcon, QPixmap +from PyQt6.QtWidgets import ( + QApplication, + QCheckBox, + QComboBox, + QDateTimeEdit, + QFormLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMainWindow, + QPushButton, + QSizePolicy, + QSpacerItem, + QTabWidget, + QVBoxLayout, + QWidget, +) from yaml import dump, load, Loader, Dumper -import customtkinter from qrcode.main import QRCode -from tkcalendar import Calendar -from tktimepicker import AnalogPicker, AnalogThemes, constants import platformdirs from .client import create_async_and_start_client, default_config @@ -34,132 +49,102 @@ except ImportError: SERVER_AVAILABLE = False -class DateAndTimePickerWindow(customtkinter.CTkToplevel): # type: ignore - def __init__( - self, - parent: customtkinter.CTkFrame | customtkinter.CTkScrollableFrame, - input_field: customtkinter.CTkTextbox, - ) -> None: - 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, - period=constants.AM if selected.hour < 12 else constants.PM, - ) - theme = AnalogThemes(self.timepicker) - theme.setDracula() - - 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(self, text="Ok", command=partial(self.insert, input_field)) - button.pack(expand=True, fill="x") - - 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): - return - hours, minutes, ampm = self.timepicker.time() - hours = hours % 12 - if ampm == "PM": - hours = hours + 12 - - 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): # 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" - ) - +# TODO: ScrollableFrame +class OptionFrame(QWidget): 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, - 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, sticky="EW") + label = QLabel(description, self) + + self.bool_options[name] = QCheckBox(self) + self.bool_options[name].setChecked(value) + self.form_layout.addRow(label, self.bool_options[name]) self.number_of_options += 1 def add_string_option( self, name: str, description: str, - value: str = "", + value: Optional[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].insert("0.0", value) + label = QLabel(description, self) + + self.string_options[name] = QLineEdit(self) + self.string_options[name].insert(value) + self.form_layout.addRow(label, self.string_options[name]) if callback is not None: - self.string_options[name].bind("", callback) - self.string_options[name].bind("", callback) + self.string_options[name].textChanged.connect(callback) self.number_of_options += 1 def del_list_element( self, name: str, - element: customtkinter.CTkTextbox, - frame: customtkinter.CTkFrame, + element: QLineEdit, + line: QWidget, + frame: QWidget, ) -> None: self.list_options[name].remove(element) - frame.destroy() + layout = frame.layout() + if layout is None: + raise ValueError("No layout found") + layout.removeWidget(line) + line.deleteLater() def add_list_element( self, name: str, - frame: customtkinter.CTkFrame, + frame: QWidget, 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) - 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) + input_and_minus = QWidget(frame) + input_and_minus_layout = QHBoxLayout(input_and_minus) + input_and_minus.setLayout(input_and_minus_layout) - minus_button = customtkinter.CTkButton( - input_and_minus, - text="-", - width=40, - command=partial(self.del_list_element, name, input_field, input_and_minus), + input_field = QLineEdit(frame) + input_field.setText(init) + input_field.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + input_and_minus_layout.addWidget(input_field) + if callback is not None: + input_field.textChanged.connect(callback) + + minus_button = QPushButton("-", frame) + minus_button.clicked.connect( + partial(self.del_list_element, name, input_field, input_and_minus, frame) ) - minus_button.pack(side="right") + minus_button.setFixedWidth(40) + minus_button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) + input_and_minus_layout.addWidget(minus_button) + + layout = frame.layout() + + if not isinstance(layout, QVBoxLayout): + raise ValueError("No layout found") + # insert second from last + layout.insertWidget(layout.count() - 1, input_and_minus) + self.list_options[name].append(input_field) + # 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: str, @@ -167,100 +152,95 @@ class OptionFrame(customtkinter.CTkScrollableFrame): # type:ignore value: list[str], callback: Optional[Callable[..., None]] = None, ) -> None: - self.add_option_label(description) + label = QLabel(description, self) - frame = customtkinter.CTkFrame(self) - frame.grid(column=1, row=self.number_of_options, sticky="EW") + container = QWidget(self) + container_layout = QVBoxLayout(container) + container.setLayout(container_layout) + + self.form_layout.addRow(label, container) + + # 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.add_list_element(name, container, v, callback) + plus_button = QPushButton("+", self) + plus_button.clicked.connect(partial(self.add_list_element, name, container, "", callback)) + plus_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + # customtkinter.CTkButton( + # frame, + # text="+", + # command=partial(self.add_list_element, name, frame, "", callback), + # ) + container_layout.addWidget(plus_button) + # plus_button.pack(side="bottom", fill="x", expand=True) self.number_of_options += 1 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(column=1, row=self.number_of_options, sticky="EW") - self.choose_options[name].set(value) - self.number_of_options += 1 + label = QLabel(description, self) - 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) - else: - self.date_and_time_pickers[name].focus() + self.choose_options[name] = QComboBox(self) + self.choose_options[name].addItems(values) + self.choose_options[name].setCurrentText(value) + self.form_layout.addRow(label, self.choose_options[name]) + self.number_of_options += 1 def add_date_time_option(self, name: str, description: str, value: str) -> None: - self.add_option_label(description) - 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 + label = QLabel(description, self) + self.date_time_options[name] = QDateTimeEdit(self) try: - datetime.fromisoformat(value) + self.date_time_options[name].setDateTime(datetime.fromisoformat(value)) except (TypeError, ValueError): - value = "" - input_field.insert("0.0", value) + self.date_time_options[name].setDateTime(datetime.now()) # TODO + + self.form_layout.addRow(label, self.date_time_options[name]) - 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: customtkinter.CTkFrame) -> None: + def __init__(self, parent: Optional[QWidget] = None) -> None: super().__init__(parent) - self.columnconfigure((1,), weight=1) + self.form_layout = QFormLayout(self) + self.setLayout(self.form_layout) 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] = {} + self.string_options: dict[str, QLineEdit] = {} + self.choose_options: dict[str, QComboBox] = {} + self.bool_options: dict[str, QCheckBox] = {} + self.list_options: dict[str, list[QLineEdit]] = {} + self.date_time_options: dict[str, QDateTimeEdit] = {} def get_config(self) -> dict[str, Any]: config: dict[str, Any] = {} for name, textbox in self.string_options.items(): - config[name] = textbox.get("0.0", "end").strip() + config[name] = textbox.text().strip() for name, optionmenu in self.choose_options.items(): - config[name] = optionmenu.get().strip() + config[name] = optionmenu.currentText().strip() for name, checkbox in self.bool_options.items(): - config[name] = checkbox.get() == 1 + config[name] = checkbox.isChecked() for name, textboxes in self.list_options.items(): config[name] = [] for textbox in textboxes: - config[name].append(textbox.get("0.0", "end").strip()) + config[name].append(textbox.text().strip()) for name, picker in self.date_time_options.items(): - config[name] = picker.get("0.0", "end").strip() + try: + config[name] = picker.dateTime().toPyDateTime().isoformat() + except ValueError: + config[name] = None return config class SourceTab(OptionFrame): - def __init__( - self, parent: customtkinter.CTkFrame, source_name: str, config: dict[str, Any] - ) -> None: + def __init__(self, parent: QWidget, source_name: str, config: dict[str, Any]) -> None: super().__init__(parent) source = available_sources[source_name] self.vars: dict[str, str | bool | list[str]] = {} @@ -278,7 +258,7 @@ class SourceTab(OptionFrame): class GeneralConfig(OptionFrame): def __init__( self, - parent: customtkinter.CTkFrame, + parent: QWidget, config: dict[str, Any], callback: Callable[..., None], ) -> None: @@ -294,7 +274,9 @@ 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 in seconds", str(config["preview_duration"]) + ) self.add_string_option("key", "Key for server", config["key"]) def get_config(self) -> dict[str, Any]: @@ -307,8 +289,8 @@ class GeneralConfig(OptionFrame): return config -class SyngGui(customtkinter.CTk): # type:ignore - def on_close(self) -> None: +class SyngGui(QMainWindow): + def closeEvent(self, a0: Optional[QCloseEvent]) -> None: if self.syng_server is not None: self.syng_server.kill() self.syng_server.join() @@ -317,64 +299,78 @@ class SyngGui(customtkinter.CTk): # type:ignore self.syng_client.terminate() self.syng_client.join() - self.withdraw() + # self.withdraw() self.destroy() def add_buttons(self) -> None: - button_line = customtkinter.CTkFrame(self) - button_line.pack(side="bottom", fill="x") + self.buttons_layout = QHBoxLayout() + self.central_layout.addLayout(self.buttons_layout) - self.startsyng_serverbutton = customtkinter.CTkButton( - button_line, text="Start Local Server", command=self.start_syng_server - ) - self.startsyng_serverbutton.pack(side="left", expand=True, anchor="w", padx=10, pady=5) + self.startsyng_serverbutton = QPushButton("Start Local Server") + self.startsyng_serverbutton.clicked.connect(self.start_syng_server) + self.buttons_layout.addWidget(self.startsyng_serverbutton) - savebutton = customtkinter.CTkButton(button_line, text="Save", command=self.save_config) - savebutton.pack(side="left", padx=10, pady=5) + spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + self.buttons_layout.addItem(spacer_item) - self.startbutton = customtkinter.CTkButton( - button_line, text="Save and Start", command=self.start_syng_client - ) - self.startbutton.pack(side="left", padx=10, pady=10) + self.savebutton = QPushButton("Save") + self.savebutton.clicked.connect(self.save_config) + self.buttons_layout.addWidget(self.savebutton) - def init_frame(self): - self.frm = customtkinter.CTkFrame(self) - self.frm.pack(ipadx=10, padx=10, fill="both", expand=True) + self.startbutton = QPushButton("Save and Start") + self.startbutton.clicked.connect(self.start_syng_client) + self.buttons_layout.addWidget(self.startbutton) - def init_tabs(self): - self.tabview = customtkinter.CTkTabview(self.frm, width=600, height=500) - self.tabview.pack(side="right", padx=10, pady=10, fill="both", expand=True) + def init_frame(self) -> None: + self.frm = QHBoxLayout() + self.central_layout.addLayout(self.frm) - self.tabview.add("General") - for source in available_sources: - self.tabview.add(source) - self.tabview.set("General") + def init_tabs(self) -> None: + self.tabview = QTabWidget(parent=self.central_widget) + self.tabview.setAcceptDrops(False) + self.tabview.setTabPosition(QTabWidget.TabPosition.West) + self.tabview.setTabShape(QTabWidget.TabShape.Rounded) + self.tabview.setDocumentMode(False) + self.tabview.setTabsClosable(False) + self.tabview.setObjectName("tabWidget") + + self.tabview.setTabText(0, "General") + for i, source in enumerate(available_sources): + self.tabview.setTabText(i + 1, source) + + self.frm.addWidget(self.tabview) def add_qr(self) -> None: - self.qrlabel = customtkinter.CTkLabel(self.frm, text="") - self.qrlabel.pack(side="top", anchor="n", padx=10, pady=10) - self.linklabel = customtkinter.CTkLabel(self.frm, text="") - self.linklabel.bind("", lambda _: self.open_web()) - self.linklabel.pack() + self.qr_widget = QWidget(parent=self.central_widget) + self.qr_layout = QVBoxLayout(self.qr_widget) + self.qr_widget.setLayout(self.qr_layout) + + self.qr_label = QLabel(self.qr_widget) + self.linklabel = QLabel(self.qr_widget) + + self.qr_layout.addWidget(self.qr_label) + self.qr_layout.addWidget(self.linklabel) + + self.linklabel.setOpenExternalLinks(True) + + self.frm.addWidget(self.qr_widget) def add_general_config(self, config: dict[str, Any]) -> None: - self.general_config = GeneralConfig(self.tabview.tab("General"), config, self.update_qr) - self.general_config.pack(ipadx=10, fill="both", expand=True) + self.general_config = GeneralConfig(self, config, self.update_qr) + self.tabview.addTab(self.general_config, "General") def add_source_config(self, source_name: str, source_config: dict[str, Any]) -> None: - self.tabs[source_name] = SourceTab( - self.tabview.tab(source_name), source_name, source_config - ) - self.tabs[source_name].pack(ipadx=10, expand=True, fill="both") + self.tabs[source_name] = SourceTab(self, source_name, source_config) + self.tabview.addTab(self.tabs[source_name], source_name) def __init__(self) -> None: - super().__init__(className="Syng") - self.protocol("WM_DELETE_WINDOW", self.on_close) + super().__init__() + self.setWindowTitle("Syng") rel_path = os.path.dirname(__file__) - img = ImageTk.PhotoImage(file=os.path.join(rel_path, "static/syng.png")) - self.wm_iconbitmap() - self.iconphoto(False, img) + qt_img = QPixmap(os.path.join(rel_path, "static/syng.png")) + qt_icon = QIcon(qt_img) + self.setWindowIcon(qt_icon) self.syng_server: Optional[Process] = None self.syng_client: Optional[Process] = None @@ -398,15 +394,15 @@ class SyngGui(customtkinter.CTk): # type:ignore config["config"]["secret"] = "".join( secrets.choice(string.ascii_letters + string.digits) for _ in range(8) ) + self.central_widget = QWidget(parent=self) + self.central_layout = QVBoxLayout(self.central_widget) - self.wm_title("Syng") - - self.add_buttons() self.init_frame() self.init_tabs() + self.add_buttons() self.add_qr() self.add_general_config(config["config"]) - self.tabs = {} + self.tabs: dict[str, SourceTab] = {} for source_name in available_sources: try: @@ -418,6 +414,8 @@ class SyngGui(customtkinter.CTk): # type:ignore self.update_qr() + self.setCentralWidget(self.central_widget) + def save_config(self) -> None: os.makedirs(os.path.dirname(self.configfile), exist_ok=True) @@ -441,12 +439,13 @@ class SyngGui(customtkinter.CTk): # type:ignore target=create_async_and_start_client, args=(config,) ) self.syng_client.start() - self.startbutton.configure(text="Stop") + self.startbutton.setText("Stop") else: self.syng_client.terminate() self.syng_client.join() self.syng_client = None - self.startbutton.configure(text="Save and Start") + # self.startbutton.configure(text="Save and Start") + self.startbutton.setText("Save and Start") def start_syng_server(self) -> None: if self.syng_server is None: @@ -459,16 +458,18 @@ class SyngGui(customtkinter.CTk): # type:ignore port=8080, registration_keyfile=None, root_folder=root_path, + private=False, + restricted=False, ) ], ) self.syng_server.start() - self.startsyng_serverbutton.configure(text="Stop Local Server") + self.startsyng_serverbutton.setText("Stop Local Server") else: self.syng_server.terminate() self.syng_server.join() self.syng_server = None - self.startsyng_serverbutton.configure(text="Start Local Server") + self.startsyng_serverbutton.setText("Start Local Server") def open_web(self) -> None: config = self.general_config.get_config() @@ -478,21 +479,29 @@ class SyngGui(customtkinter.CTk): # type:ignore webbrowser.open(syng_server + room) def change_qr(self, data: str) -> None: - qr = QRCode(box_size=20, border=2) + qr = QRCode(box_size=10, border=2) qr.add_data(data) qr.make() image = qr.make_image().convert("RGB") - tk_qrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) - self.qrlabel.configure(image=tk_qrcode) + buf = BytesIO() + image.save(buf, "PNG") + qr_pixmap = QPixmap() + qr_pixmap.loadFromData(buf.getvalue(), "PNG") + self.qr_label.setPixmap(qr_pixmap) - def update_qr(self, _evt: None = None) -> None: + def update_qr(self) -> None: config = self.general_config.get_config() syng_server = config["server"] syng_server += "" if syng_server.endswith("/") else "/" room = config["room"] - self.linklabel.configure(text=syng_server + room) + self.linklabel.setText(f'{syng_server + room}') self.change_qr(syng_server + room) def run_gui() -> None: - SyngGui().mainloop() + app = QApplication([]) + app.setApplicationName("Syng") + app.setDesktopFileName("rocks.syng.Syng") + window = SyngGui() + window.show() + app.exec()