Implemented GUI for syng #2

Merged
christofsteel merged 18 commits from gui into main 2023-12-18 19:48:28 +01:00
6 changed files with 133 additions and 23 deletions
Showing only changes of commit f5a8b16a7f - Show all commits

View file

@ -10,6 +10,7 @@ include = ["syng/static"]
[tool.poetry.scripts]
syng-client = "syng.client:main"
syng-server = "syng.server:main"
syng-gui = "syng.gui:main"
syng-shell = "syng.webclientmockup:main"
[tool.poetry.dependencies]
@ -27,6 +28,8 @@ qrcode = "^7.4.2"
pymediainfo = "^6.1.0"
pyyaml = "^6.0.1"
async-tkinter-loop = "^0.9.2"
tkcalendar = "^1.6.1"
tktimepicker = "^2.0.2"
[build-system]
requires = ["poetry-core"]

View file

@ -65,6 +65,8 @@ currentLock: asyncio.Semaphore = asyncio.Semaphore(0)
def default_config() -> dict[str, Optional[int | str]]:
return {
"server": "http://localhost:8080",
"room": "ABCD",
"preview_duration": 3,
"secret": None,
"last_song": None,
@ -416,6 +418,9 @@ async def aiomain() -> None:
"""
pass
def create_async_and_start_client(config):
asyncio.run(start_client(config))
def main() -> None:
"""Entry point for the syng-client script."""
@ -443,7 +448,7 @@ def main() -> None:
if args.server:
config["config"] |= {"server": args.server}
asyncio.run(start_client(config))
create_async_and_start_client(config)
if __name__ == "__main__":

View file

@ -1,19 +1,57 @@
import asyncio
from datetime import datetime, date, time
import os
import builtins
from functools import partial
import webbrowser
import PIL
from yaml import load, Loader
import multiprocessing
import customtkinter
import qrcode
import secrets
import string
from tkinter import filedialog
from async_tkinter_loop import async_handler, async_mainloop
from async_tkinter_loop.mixins import AsyncCTk
from tkinter import PhotoImage, filedialog
from tkcalendar import Calendar
from tktimepicker import SpinTimePickerOld, AnalogPicker, AnalogThemes
from tktimepicker import constants
from .client import default_config, start_client
from .client import create_async_and_start_client, default_config, start_client
from .sources import available_sources
from .server import main as server_main
class DateAndTimePickerWindow(customtkinter.CTkToplevel):
def __init__(self, parent, input_field):
super().__init__(parent)
self.calendar = Calendar(self)
self.calendar.pack(expand=True, fill="both")
self.timepicker = AnalogPicker(self, type=constants.HOURS12)
theme = AnalogThemes(self.timepicker)
theme.setDracula()
# self.timepicker.addAll(constants.HOURS24)
self.timepicker.pack(expand=True, fill="both")
button = customtkinter.CTkButton(self, text="Ok", command=partial(self.insert, input_field))
button.pack(expand=True, fill='x')
def insert(self, input_field: customtkinter.CTkTextbox):
input_field.delete("0.0", "end")
selected_date = self.calendar.selection_get()
print(type(selected_date))
if not isinstance(selected_date, date):
return
hours, minutes, ampm = self.timepicker.time()
if ampm == "PM":
hours = (hours + 12) % 24
selected_datetime = datetime.combine(selected_date, time(hours, minutes))
input_field.insert("0.0", selected_datetime.isoformat())
self.withdraw()
self.destroy()
class OptionFrame(customtkinter.CTkScrollableFrame):
@ -104,6 +142,35 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
self.choose_options[name].set(value)
self.number_of_options += 1
def open_date_and_time_picker(self, name, input_field):
if name not in self.date_and_time_pickers or not self.date_and_time_pickers[name].winfo_exists():
self.date_and_time_pickers[name] = DateAndTimePickerWindow(self, input_field)
else:
self.date_and_time_pickers[name].focus()
def add_date_time_option(self, name, description, value):
self.add_option_label(description)
self.date_time_options[name] = None
input_and_button = customtkinter.CTkFrame(self)
input_and_button.grid(column=1, row=self.number_of_options, sticky="EW")
input_field = customtkinter.CTkTextbox(input_and_button, wrap="none", height=1)
input_field.pack(side="left", fill="x", expand=True)
try:
datetime.fromisoformat(value)
except TypeError:
value = ""
input_field.insert("0.0", value)
button = customtkinter.CTkButton(
input_and_button,
text="...",
width=40,
command=partial(self.open_date_and_time_picker, name, input_field),
)
button.pack(side="right")
self.number_of_options += 1
def __init__(self, parent):
super().__init__(parent)
self.columnconfigure((1,), weight=1)
@ -112,6 +179,8 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
self.choose_options = {}
self.bool_options = {}
self.list_options = {}
self.date_time_options = {}
self.date_and_time_pickers = {}
def get_config(self):
config = {}
@ -173,9 +242,10 @@ class GeneralConfig(OptionFrame):
["forced", "optional", "none"],
str(config["waiting_room_policy"]).lower(),
)
self.add_string_option(
"last_song", "Time of last song\nin ISO-8601", config["last_song"]
)
# self.add_string_option(
# "last_song", "Time of last song\nin ISO-8601", config["last_song"]
# )
self.add_date_time_option("last_song", "Time of last song", config["last_song"])
self.add_string_option(
"preview_duration", "Preview Duration", config["preview_duration"]
)
@ -190,15 +260,37 @@ class GeneralConfig(OptionFrame):
return config
class SyngGui(customtkinter.CTk, AsyncCTk):
class SyngGui(customtkinter.CTk):
def loadConfig(self):
filedialog.askopenfilename()
def on_close(self):
if self.server is not None:
self.server.kill()
if self.client is not None:
self.client.kill()
self.withdraw()
self.destroy()
def __init__(self):
super().__init__(className="Syng")
self.protocol("WM_DELETE_WINDOW", self.on_close)
rel_path = os.path.dirname(__file__)
img = PIL.ImageTk.PhotoImage(file=os.path.join(rel_path,"static/syng.png"))
self.wm_iconbitmap()
self.iconphoto(False, img)
self.server = None
self.client = None
try:
with open("syng-client.yaml") as cfile:
loaded_config = load(cfile, Loader=Loader)
except FileNotFoundError:
loaded_config = {}
config = {"sources": {}, "config": default_config()}
if "config" in loaded_config:
config["config"] |= loaded_config["config"]
@ -222,10 +314,16 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
loadbutton.pack(side="left")
startbutton = customtkinter.CTkButton(
fileframe, text="Start", command=self.start
fileframe, text="Start", command=self.start_client
)
startbutton.pack(side="right")
startserverbutton = customtkinter.CTkButton(
fileframe, text="Start Server", command=self.start_server
)
startserverbutton.pack(side="right")
open_web_button = customtkinter.CTkButton(
fileframe, text="Open Web", command=self.open_web
)
@ -266,8 +364,7 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
self.updateQr()
@async_handler
async def start(self):
def start_client(self):
sources = {}
for source, tab in self.tabs.items():
sources[source] = tab.get_config()
@ -275,8 +372,14 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
general_config = self.general_config.get_config()
config = {"sources": sources, "config": general_config}
print(config)
await start_client(config)
# print(config)
self.client = multiprocessing.Process(target=create_async_and_start_client, args=(config,))
self.client.start()
def start_server(self):
self.server = multiprocessing.Process(target=server_main)
self.server.start()
def open_web(self):
config = self.general_config.get_config()
@ -303,11 +406,9 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
self.changeQr(server + room)
# async def main():
# gui = SyngGui()
# await gui.run()
def main():
SyngGui().mainloop()
if __name__ == "__main__":
# asyncio.run(main())
SyngGui().async_mainloop()
main()

View file

@ -26,5 +26,6 @@ def configure_sources(configs: dict[str, Any]) -> dict[str, Source]:
configured_sources = {}
for source, config in configs.items():
if source in available_sources:
if config["enabled"]:
configured_sources[source] = available_sources[source](config)
return configured_sources

View file

@ -19,7 +19,7 @@ class FileBasedSource(Source):
"extensions": (
list,
"List of filename extensions\n(mp3+cdg, mp4, ...)",
"mp3+cdg",
["mp3+cdg"],
)
}

BIN
syng/static/syng.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB