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-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"

View file

@ -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]

View file

@ -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("<KeyRelease>", callback)
self.string_options[name].bind("<ButtonRelease>", 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("<KeyRelease>", callback)
input_field.bind("<ButtonRelease>", 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("<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(
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("<Button-1>", 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'<a href="{syng_server + room}">{syng_server + room}</a>')
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()