removed tk and added qt gui

This commit is contained in:
Christoph Stahl 2024-09-22 13:02:09 +02:00
parent 7689172494
commit 50585463fc
3 changed files with 291 additions and 288 deletions

136
poetry.lock generated
View file

@ -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 = ["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"] 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]] [[package]]
name = "bidict" name = "bidict"
version = "0.23.1" version = "0.23.1"
@ -608,35 +594,6 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {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]] [[package]]
name = "dill" name = "dill"
version = "0.3.8" version = "0.3.8"
@ -1315,6 +1272,70 @@ files = [
{file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"}, {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]] [[package]]
name = "python-engineio" name = "python-engineio"
version = "4.9.1" version = "4.9.1"
@ -1575,31 +1596,6 @@ files = [
{file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, {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]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" 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)"] test = ["pytest (>=8.1,<9.0)"]
[extras] [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"] server = ["alt-profanity-check"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "3310779a7a61511f14b32d0008db391c5eb0200c67187e61fd4d46cd1ba17677" content-hash = "2c3d2e35ed8bf2563a078971cede2cc5ba60f9802631ee6f1f5761777dc7e3a5"

View file

@ -18,15 +18,13 @@ yt-dlp = { version = "*"}
minio = { version = "^7.2.0", optional = true } minio = { version = "^7.2.0", optional = true }
mutagen = { version = "^1.47.0", optional = true } mutagen = { version = "^1.47.0", optional = true }
pillow = { version = "^10.1.0", optional = true} pillow = { version = "^10.1.0", optional = true}
customtkinter = { version = "^5.2.1", optional = true}
qrcode = { version = "^7.4.2", optional = true } qrcode = { version = "^7.4.2", optional = true }
pymediainfo = { version = "^6.1.0", optional = true } pymediainfo = { version = "^6.1.0", optional = true }
pyyaml = { version = "^6.0.1", 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 } platformdirs = { version = "^4.0.0", optional = true }
packaging = {version = "^23.2", optional = true} packaging = {version = "^23.2", optional = true}
alt-profanity-check = {version = "^1.4.1", optional = true} alt-profanity-check = {version = "^1.4.1", optional = true}
pyqt6 = {version = "^6.7.1", optional = true}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
types-pyyaml = "^6.0.12.12" types-pyyaml = "^6.0.12.12"
@ -36,9 +34,9 @@ pylint = "^3.2.7"
[tool.poetry.extras] [tool.poetry.extras]
client = ["minio", "mutagen", "pillow", "customtkinter", "qrcode", client = ["minio", "mutagen", "pillow", "qrcode",
"pymediainfo", "pyyaml", "tkcalendar", "tktimepicker", "platformdirs", "pymediainfo", "pyyaml", "platformdirs",
"packaging"] "packaging", "pyqt6"]
server = ["alt-profanity-check"] server = ["alt-profanity-check"]
[build-system] [build-system]

View file

@ -1,7 +1,8 @@
from argparse import Namespace from argparse import Namespace
from io import BytesIO
from multiprocessing import Process from multiprocessing import Process
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, date, time from datetime import datetime
import os import os
import builtins import builtins
from functools import partial from functools import partial
@ -11,12 +12,26 @@ import multiprocessing
import secrets import secrets
import string 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 from yaml import dump, load, Loader, Dumper
import customtkinter
from qrcode.main import QRCode from qrcode.main import QRCode
from tkcalendar import Calendar
from tktimepicker import AnalogPicker, AnalogThemes, constants
import platformdirs import platformdirs
from .client import create_async_and_start_client, default_config from .client import create_async_and_start_client, default_config
@ -34,132 +49,102 @@ except ImportError:
SERVER_AVAILABLE = False SERVER_AVAILABLE = False
class DateAndTimePickerWindow(customtkinter.CTkToplevel): # type: ignore # TODO: ScrollableFrame
def __init__( class OptionFrame(QWidget):
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"
)
def add_bool_option(self, name: str, description: str, value: bool = False) -> None: def add_bool_option(self, name: str, description: str, value: bool = False) -> None:
self.add_option_label(description) label = QLabel(description, self)
self.bool_options[name] = customtkinter.CTkCheckBox(
self, self.bool_options[name] = QCheckBox(self)
text="", self.bool_options[name].setChecked(value)
onvalue=True, self.form_layout.addRow(label, self.bool_options[name])
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")
self.number_of_options += 1 self.number_of_options += 1
def add_string_option( def add_string_option(
self, self,
name: str, name: str,
description: str, description: str,
value: str = "", value: Optional[str] = "",
callback: Optional[Callable[..., None]] = None, callback: Optional[Callable[..., None]] = None,
) -> None: ) -> None:
self.add_option_label(description)
if value is None: if value is None:
value = "" value = ""
self.string_options[name] = customtkinter.CTkTextbox(self, wrap="none", height=1) label = QLabel(description, self)
self.string_options[name].grid(column=1, row=self.number_of_options, sticky="EW")
self.string_options[name].insert("0.0", value) 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: if callback is not None:
self.string_options[name].bind("<KeyRelease>", callback) self.string_options[name].textChanged.connect(callback)
self.string_options[name].bind("<ButtonRelease>", callback)
self.number_of_options += 1 self.number_of_options += 1
def del_list_element( def del_list_element(
self, self,
name: str, name: str,
element: customtkinter.CTkTextbox, element: QLineEdit,
frame: customtkinter.CTkFrame, line: QWidget,
frame: QWidget,
) -> None: ) -> None:
self.list_options[name].remove(element) 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( def add_list_element(
self, self,
name: str, name: str,
frame: customtkinter.CTkFrame, frame: QWidget,
init: str, init: str,
callback: Optional[Callable[..., None]], callback: Optional[Callable[..., None]],
) -> None: ) -> None:
input_and_minus = customtkinter.CTkFrame(frame) input_and_minus = QWidget(frame)
input_and_minus.pack(side="top", fill="x", expand=True) input_and_minus_layout = QHBoxLayout(input_and_minus)
input_field = customtkinter.CTkTextbox(input_and_minus, wrap="none", height=1) input_and_minus.setLayout(input_and_minus_layout)
input_field.pack(side="left", fill="x", expand=True)
input_field.insert("0.0", init)
if callback is not None:
input_field.bind("<KeyRelease>", callback)
input_field.bind("<ButtonRelease>", callback)
minus_button = customtkinter.CTkButton( input_field = QLineEdit(frame)
input_and_minus, input_field.setText(init)
text="-", input_field.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
width=40, input_and_minus_layout.addWidget(input_field)
command=partial(self.del_list_element, name, input_field, input_and_minus), 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) 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("<KeyRelease>", callback)
# input_field.bind("<ButtonRelease>", 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( def add_list_option(
self, self,
name: str, name: str,
@ -167,100 +152,95 @@ class OptionFrame(customtkinter.CTkScrollableFrame): # type:ignore
value: list[str], value: list[str],
callback: Optional[Callable[..., None]] = None, callback: Optional[Callable[..., None]] = None,
) -> None: ) -> None:
self.add_option_label(description) label = QLabel(description, self)
frame = customtkinter.CTkFrame(self) container = QWidget(self)
frame.grid(column=1, row=self.number_of_options, sticky="EW") 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] = [] self.list_options[name] = []
for v in value: for v in value:
self.add_list_element(name, frame, v, callback) self.add_list_element(name, container, v, callback)
plus_button = customtkinter.CTkButton( plus_button = QPushButton("+", self)
frame, plus_button.clicked.connect(partial(self.add_list_element, name, container, "", callback))
text="+", plus_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
command=partial(self.add_list_element, name, frame, "", callback),
) # customtkinter.CTkButton(
plus_button.pack(side="bottom", fill="x", expand=True) # 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 self.number_of_options += 1
def add_choose_option( def add_choose_option(
self, name: str, description: str, values: list[str], value: str = "" self, name: str, description: str, values: list[str], value: str = ""
) -> None: ) -> None:
self.add_option_label(description) label = QLabel(description, self)
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
def open_date_and_time_picker(self, name: str, input_field: customtkinter.CTkTextbox) -> None: self.choose_options[name] = QComboBox(self)
if ( self.choose_options[name].addItems(values)
name not in self.date_and_time_pickers self.choose_options[name].setCurrentText(value)
or not self.date_and_time_pickers[name].winfo_exists() self.form_layout.addRow(label, self.choose_options[name])
): self.number_of_options += 1
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: def add_date_time_option(self, name: str, description: str, value: str) -> None:
self.add_option_label(description) label = QLabel(description, self)
input_and_button = customtkinter.CTkFrame(self) self.date_time_options[name] = QDateTimeEdit(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: try:
datetime.fromisoformat(value) self.date_time_options[name].setDateTime(datetime.fromisoformat(value))
except (TypeError, ValueError): except (TypeError, ValueError):
value = "" self.date_time_options[name].setDateTime(datetime.now()) # TODO
input_field.insert("0.0", value)
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 self.number_of_options += 1
def __init__(self, parent: customtkinter.CTkFrame) -> None: def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent) 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.number_of_options: int = 0
self.string_options: dict[str, customtkinter.CTkTextbox] = {} self.string_options: dict[str, QLineEdit] = {}
self.choose_options: dict[str, customtkinter.CTkOptionMenu] = {} self.choose_options: dict[str, QComboBox] = {}
self.bool_options: dict[str, customtkinter.CTkCheckBox] = {} self.bool_options: dict[str, QCheckBox] = {}
self.list_options: dict[str, list[customtkinter.CTkTextbox]] = {} self.list_options: dict[str, list[QLineEdit]] = {}
self.date_time_options: dict[str, customtkinter.CTkTextbox] = {} self.date_time_options: dict[str, QDateTimeEdit] = {}
self.date_and_time_pickers: dict[str, DateAndTimePickerWindow] = {}
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
config: dict[str, Any] = {} config: dict[str, Any] = {}
for name, textbox in self.string_options.items(): 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(): 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(): 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(): for name, textboxes in self.list_options.items():
config[name] = [] config[name] = []
for textbox in textboxes: 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(): 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 return config
class SourceTab(OptionFrame): class SourceTab(OptionFrame):
def __init__( def __init__(self, parent: QWidget, source_name: str, config: dict[str, Any]) -> None:
self, parent: customtkinter.CTkFrame, source_name: str, config: dict[str, Any]
) -> None:
super().__init__(parent) super().__init__(parent)
source = available_sources[source_name] source = available_sources[source_name]
self.vars: dict[str, str | bool | list[str]] = {} self.vars: dict[str, str | bool | list[str]] = {}
@ -278,7 +258,7 @@ class SourceTab(OptionFrame):
class GeneralConfig(OptionFrame): class GeneralConfig(OptionFrame):
def __init__( def __init__(
self, self,
parent: customtkinter.CTkFrame, parent: QWidget,
config: dict[str, Any], config: dict[str, Any],
callback: Callable[..., None], callback: Callable[..., None],
) -> None: ) -> None:
@ -294,7 +274,9 @@ class GeneralConfig(OptionFrame):
str(config["waiting_room_policy"]).lower(), str(config["waiting_room_policy"]).lower(),
) )
self.add_date_time_option("last_song", "Time of last song", 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"]) self.add_string_option(
"preview_duration", "Preview duration in seconds", str(config["preview_duration"])
)
self.add_string_option("key", "Key for server", config["key"]) self.add_string_option("key", "Key for server", config["key"])
def get_config(self) -> dict[str, Any]: def get_config(self) -> dict[str, Any]:
@ -307,8 +289,8 @@ class GeneralConfig(OptionFrame):
return config return config
class SyngGui(customtkinter.CTk): # type:ignore class SyngGui(QMainWindow):
def on_close(self) -> None: def closeEvent(self, a0: Optional[QCloseEvent]) -> None:
if self.syng_server is not None: if self.syng_server is not None:
self.syng_server.kill() self.syng_server.kill()
self.syng_server.join() self.syng_server.join()
@ -317,64 +299,78 @@ class SyngGui(customtkinter.CTk): # type:ignore
self.syng_client.terminate() self.syng_client.terminate()
self.syng_client.join() self.syng_client.join()
self.withdraw() # self.withdraw()
self.destroy() self.destroy()
def add_buttons(self) -> None: def add_buttons(self) -> None:
button_line = customtkinter.CTkFrame(self) self.buttons_layout = QHBoxLayout()
button_line.pack(side="bottom", fill="x") self.central_layout.addLayout(self.buttons_layout)
self.startsyng_serverbutton = customtkinter.CTkButton( self.startsyng_serverbutton = QPushButton("Start Local Server")
button_line, text="Start Local Server", command=self.start_syng_server self.startsyng_serverbutton.clicked.connect(self.start_syng_server)
) self.buttons_layout.addWidget(self.startsyng_serverbutton)
self.startsyng_serverbutton.pack(side="left", expand=True, anchor="w", padx=10, pady=5)
savebutton = customtkinter.CTkButton(button_line, text="Save", command=self.save_config) spacer_item = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
savebutton.pack(side="left", padx=10, pady=5) self.buttons_layout.addItem(spacer_item)
self.startbutton = customtkinter.CTkButton( self.savebutton = QPushButton("Save")
button_line, text="Save and Start", command=self.start_syng_client self.savebutton.clicked.connect(self.save_config)
) self.buttons_layout.addWidget(self.savebutton)
self.startbutton.pack(side="left", padx=10, pady=10)
def init_frame(self): self.startbutton = QPushButton("Save and Start")
self.frm = customtkinter.CTkFrame(self) self.startbutton.clicked.connect(self.start_syng_client)
self.frm.pack(ipadx=10, padx=10, fill="both", expand=True) self.buttons_layout.addWidget(self.startbutton)
def init_tabs(self): def init_frame(self) -> None:
self.tabview = customtkinter.CTkTabview(self.frm, width=600, height=500) self.frm = QHBoxLayout()
self.tabview.pack(side="right", padx=10, pady=10, fill="both", expand=True) self.central_layout.addLayout(self.frm)
self.tabview.add("General") def init_tabs(self) -> None:
for source in available_sources: self.tabview = QTabWidget(parent=self.central_widget)
self.tabview.add(source) self.tabview.setAcceptDrops(False)
self.tabview.set("General") 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: def add_qr(self) -> None:
self.qrlabel = customtkinter.CTkLabel(self.frm, text="") self.qr_widget = QWidget(parent=self.central_widget)
self.qrlabel.pack(side="top", anchor="n", padx=10, pady=10) self.qr_layout = QVBoxLayout(self.qr_widget)
self.linklabel = customtkinter.CTkLabel(self.frm, text="") self.qr_widget.setLayout(self.qr_layout)
self.linklabel.bind("<Button-1>", lambda _: self.open_web())
self.linklabel.pack() 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: 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 = GeneralConfig(self, config, self.update_qr)
self.general_config.pack(ipadx=10, fill="both", expand=True) self.tabview.addTab(self.general_config, "General")
def add_source_config(self, source_name: str, source_config: dict[str, Any]) -> None: def add_source_config(self, source_name: str, source_config: dict[str, Any]) -> None:
self.tabs[source_name] = SourceTab( self.tabs[source_name] = SourceTab(self, source_name, source_config)
self.tabview.tab(source_name), source_name, source_config self.tabview.addTab(self.tabs[source_name], source_name)
)
self.tabs[source_name].pack(ipadx=10, expand=True, fill="both")
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(className="Syng") super().__init__()
self.protocol("WM_DELETE_WINDOW", self.on_close) self.setWindowTitle("Syng")
rel_path = os.path.dirname(__file__) rel_path = os.path.dirname(__file__)
img = ImageTk.PhotoImage(file=os.path.join(rel_path, "static/syng.png")) qt_img = QPixmap(os.path.join(rel_path, "static/syng.png"))
self.wm_iconbitmap() qt_icon = QIcon(qt_img)
self.iconphoto(False, img) self.setWindowIcon(qt_icon)
self.syng_server: Optional[Process] = None self.syng_server: Optional[Process] = None
self.syng_client: Optional[Process] = None self.syng_client: Optional[Process] = None
@ -398,15 +394,15 @@ class SyngGui(customtkinter.CTk): # type:ignore
config["config"]["secret"] = "".join( config["config"]["secret"] = "".join(
secrets.choice(string.ascii_letters + string.digits) for _ in range(8) 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_frame()
self.init_tabs() self.init_tabs()
self.add_buttons()
self.add_qr() self.add_qr()
self.add_general_config(config["config"]) self.add_general_config(config["config"])
self.tabs = {} self.tabs: dict[str, SourceTab] = {}
for source_name in available_sources: for source_name in available_sources:
try: try:
@ -418,6 +414,8 @@ class SyngGui(customtkinter.CTk): # type:ignore
self.update_qr() self.update_qr()
self.setCentralWidget(self.central_widget)
def save_config(self) -> None: def save_config(self) -> None:
os.makedirs(os.path.dirname(self.configfile), exist_ok=True) 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,) target=create_async_and_start_client, args=(config,)
) )
self.syng_client.start() self.syng_client.start()
self.startbutton.configure(text="Stop") self.startbutton.setText("Stop")
else: else:
self.syng_client.terminate() self.syng_client.terminate()
self.syng_client.join() self.syng_client.join()
self.syng_client = None 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: def start_syng_server(self) -> None:
if self.syng_server is None: if self.syng_server is None:
@ -459,16 +458,18 @@ class SyngGui(customtkinter.CTk): # type:ignore
port=8080, port=8080,
registration_keyfile=None, registration_keyfile=None,
root_folder=root_path, root_folder=root_path,
private=False,
restricted=False,
) )
], ],
) )
self.syng_server.start() self.syng_server.start()
self.startsyng_serverbutton.configure(text="Stop Local Server") self.startsyng_serverbutton.setText("Stop Local Server")
else: else:
self.syng_server.terminate() self.syng_server.terminate()
self.syng_server.join() self.syng_server.join()
self.syng_server = None self.syng_server = None
self.startsyng_serverbutton.configure(text="Start Local Server") self.startsyng_serverbutton.setText("Start Local Server")
def open_web(self) -> None: def open_web(self) -> None:
config = self.general_config.get_config() config = self.general_config.get_config()
@ -478,21 +479,29 @@ class SyngGui(customtkinter.CTk): # type:ignore
webbrowser.open(syng_server + room) webbrowser.open(syng_server + room)
def change_qr(self, data: str) -> None: 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.add_data(data)
qr.make() qr.make()
image = qr.make_image().convert("RGB") image = qr.make_image().convert("RGB")
tk_qrcode = customtkinter.CTkImage(light_image=image, size=(280, 280)) buf = BytesIO()
self.qrlabel.configure(image=tk_qrcode) 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() config = self.general_config.get_config()
syng_server = config["server"] syng_server = config["server"]
syng_server += "" if syng_server.endswith("/") else "/" syng_server += "" if syng_server.endswith("/") else "/"
room = config["room"] room = config["room"]
self.linklabel.configure(text=syng_server + room) self.linklabel.setText(f'<a href="{syng_server + room}">{syng_server + room}</a>')
self.change_qr(syng_server + room) self.change_qr(syng_server + room)
def run_gui() -> None: def run_gui() -> None:
SyngGui().mainloop() app = QApplication([])
app.setApplicationName("Syng")
app.setDesktopFileName("rocks.syng.Syng")
window = SyngGui()
window.show()
app.exec()