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

View file

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

View file

@ -1,19 +1,57 @@
import asyncio import asyncio
from datetime import datetime, date, time
import os
import builtins import builtins
from functools import partial from functools import partial
import webbrowser import webbrowser
import PIL
from yaml import load, Loader from yaml import load, Loader
import multiprocessing
import customtkinter import customtkinter
import qrcode import qrcode
import secrets import secrets
import string import string
from tkinter import filedialog from tkinter import PhotoImage, filedialog
from async_tkinter_loop import async_handler, async_mainloop from tkcalendar import Calendar
from async_tkinter_loop.mixins import AsyncCTk 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 .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): class OptionFrame(customtkinter.CTkScrollableFrame):
@ -104,6 +142,35 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
self.choose_options[name].set(value) self.choose_options[name].set(value)
self.number_of_options += 1 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): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self.columnconfigure((1,), weight=1) self.columnconfigure((1,), weight=1)
@ -112,6 +179,8 @@ class OptionFrame(customtkinter.CTkScrollableFrame):
self.choose_options = {} self.choose_options = {}
self.bool_options = {} self.bool_options = {}
self.list_options = {} self.list_options = {}
self.date_time_options = {}
self.date_and_time_pickers = {}
def get_config(self): def get_config(self):
config = {} config = {}
@ -173,9 +242,10 @@ class GeneralConfig(OptionFrame):
["forced", "optional", "none"], ["forced", "optional", "none"],
str(config["waiting_room_policy"]).lower(), str(config["waiting_room_policy"]).lower(),
) )
self.add_string_option( # self.add_string_option(
"last_song", "Time of last song\nin ISO-8601", config["last_song"] # "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( self.add_string_option(
"preview_duration", "Preview Duration", config["preview_duration"] "preview_duration", "Preview Duration", config["preview_duration"]
) )
@ -190,15 +260,37 @@ class GeneralConfig(OptionFrame):
return config return config
class SyngGui(customtkinter.CTk, AsyncCTk): class SyngGui(customtkinter.CTk):
def loadConfig(self): def loadConfig(self):
filedialog.askopenfilename() 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): def __init__(self):
super().__init__(className="Syng") super().__init__(className="Syng")
self.protocol("WM_DELETE_WINDOW", self.on_close)
with open("syng-client.yaml") as cfile: rel_path = os.path.dirname(__file__)
loaded_config = load(cfile, Loader=Loader) 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()} config = {"sources": {}, "config": default_config()}
if "config" in loaded_config: if "config" in loaded_config:
config["config"] |= loaded_config["config"] config["config"] |= loaded_config["config"]
@ -222,10 +314,16 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
loadbutton.pack(side="left") loadbutton.pack(side="left")
startbutton = customtkinter.CTkButton( startbutton = customtkinter.CTkButton(
fileframe, text="Start", command=self.start fileframe, text="Start", command=self.start_client
) )
startbutton.pack(side="right") startbutton.pack(side="right")
startserverbutton = customtkinter.CTkButton(
fileframe, text="Start Server", command=self.start_server
)
startserverbutton.pack(side="right")
open_web_button = customtkinter.CTkButton( open_web_button = customtkinter.CTkButton(
fileframe, text="Open Web", command=self.open_web fileframe, text="Open Web", command=self.open_web
) )
@ -266,8 +364,7 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
self.updateQr() self.updateQr()
@async_handler def start_client(self):
async def start(self):
sources = {} sources = {}
for source, tab in self.tabs.items(): for source, tab in self.tabs.items():
sources[source] = tab.get_config() sources[source] = tab.get_config()
@ -275,8 +372,14 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
general_config = self.general_config.get_config() general_config = self.general_config.get_config()
config = {"sources": sources, "config": general_config} config = {"sources": sources, "config": general_config}
print(config) # print(config)
await start_client(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): def open_web(self):
config = self.general_config.get_config() config = self.general_config.get_config()
@ -303,11 +406,9 @@ class SyngGui(customtkinter.CTk, AsyncCTk):
self.changeQr(server + room) self.changeQr(server + room)
# async def main(): def main():
# gui = SyngGui() SyngGui().mainloop()
# await gui.run()
if __name__ == "__main__": if __name__ == "__main__":
# asyncio.run(main()) main()
SyngGui().async_mainloop()

View file

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

View file

@ -19,7 +19,7 @@ class FileBasedSource(Source):
"extensions": ( "extensions": (
list, list,
"List of filename extensions\n(mp3+cdg, mp4, ...)", "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